# Current

> Keep this file short. One active step, one ordered backlog. Completed work moves to
> [progress.md](progress.md). If this file starts reading like a changelog, it has
> drifted — that's exactly what went wrong last time.

## Active: Milestone 3 — Repo model

**Goal:** repositories exist as records and as bare git repos on disk, and they appear
on the profile. No git protocol yet — that is milestone 4. This milestone fills the
Repositories section and gets the storage layout right before anything serves it.

**Out of scope:** clone, push, browsing a tree, README rendering, forks, stars.
Deleting a repo — worth having, but it makes the filesystem/database consistency
problem twice as interesting, so not in the first pass.

### Steps

- [x] Domain: `RepoId`, `RepoName`, `Visibility` (Public/Private), `Repository`
- [x] Domain: `RepoRepository` port — `find_by_id`, `find_by_org_and_name`,
      `list_by_org`, `save`
- [x] Infrastructure: in-memory + SQLite implementations, migration
- [x] Application: `GitStorage` port — `init_bare`, `remove`, `repo_path`
- [x] Infrastructure: `DiskGitStorage`, shelling out to `git init --bare`
- [ ] Application: `create_repo` use case — owner only, validates, creates record and
      bare repo
- [ ] Application: `list_repos` / `view_repo` read models — visibility-aware
- [ ] Web: `/{handle}/repos/new` form, `/{handle}/repos/{name}` page
- [ ] Web: the profile's Repositories section lists what the viewer may see
- [ ] `/api/users/{handle}/repos`

### Done when

The owner creates a repo through the UI, a bare repo appears at
`{data_dir}/{handle}/{name}.git`, and it is listed on the profile. A private repo is
invisible to a signed-out visitor. `git clone` does **not** work yet — that is
milestone 4.

### Settled

- **Repo name rules:** `OrgName`'s, plus `.` and `_` for names like `.github` and
  `foo.js`. Lowercased, max 100. Also rejects a name of nothing but dots and any name
  ending `.git` — the first is traversal, the second would live at `foo.git.git`.
- **Reserved repo names:** `import`, `new`, `search`. Only names directly under
  `/{handle}/repos/` can collide.
- **Visibility defaults to public**, matching a portfolio-first product.
- **Repositories carry an optional description**, capped at 300 characters — a sentence
  for the profile listing, not a README. Kept deliberately: portfolio-first is the
  tie-break, and this milestone's own "Done when" puts repositories on the profile, so
  the consumer is inside the milestone rather than hypothetical. The per-repo analogue
  of `Organization::bio`.
- **`list_by_org` returns every repository regardless of visibility.** Filtering is an
  authorization decision and belongs to the use case, so the page and `/api` cannot end
  up applying different rules. The cost is that a private repo is briefly in memory
  before being filtered, which is fine in-process.

### Open

Nothing open. `GitStorage`'s shape and how git is invoked are recorded in
[0006](decisions/0006-git-binary-behind-narrow-ports.md).

### Watch for

- **The database and the filesystem cannot share a transaction.** Creating a repo
  writes a row and a directory. Neither previous attempt solved this properly — see
  [architecture.md](architecture.md#db-plus-filesystem-writes). A compensating delete is
  good enough to ship, but write down that an orphaned directory is possible if the
  process dies between the two, rather than rediscovering it.
- **Path traversal.** `{data_dir}/{handle}/{name}.git` is built from user input. A name
  containing `..` or `/` must be impossible before it reaches the filesystem, and
  `RepoName` is the place to make it impossible rather than sanitising at the call site.
- **Visibility is an authorization decision**, so it belongs in the use case. A private
  repo must be absent from listings, not merely unlinked — and `/api` must agree with
  the page.
- **`git` is a dependency of the test suite too**, not only of the runtime —
  `DiskGitStorage`'s tests run real `git init`. A machine without `git` fails
  `cargo test`, not just the app.
- **A handle rename is a directory move.** The layout is keyed by handle for
  legibility, so whenever renaming arrives it has to move the tree; it cannot be a row
  update. Nothing renames handles today.
- **Bare repos created on macOS carry `ignorecase = true`** in their config, because
  git probes the filesystem at init. Correct where it was created, wrong if the data
  directory is ever moved to Linux. A migration gotcha, not a bug.
- **An orphaned directory blocks re-creating that name.** `init_bare` refuses rather
  than adopting what is already there, and repo deletion is out of scope this
  milestone, so clearing one is a manual `rm` for now.

### Carried over — small, unblocked

- **Fonts are not loaded.** The theme names Geist and IBM Plex Mono; both fall back
  today. Topcoat's `font-fontsource` feature handles it.
- **Light mode is untested.** The palette defines it; nobody has looked at it.
- **No rate limiting** on `/auth/login` or `/auth/setup`.
- **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is
  enforced on read, so this is tidiness, not a hole.
- **CSRF.** `SameSite=Lax` covers the common case. Forms now exist, so this is decidable
  rather than hypothetical.

## Backlog

Ordered. Pull from the top.

1. **Milestone 4 — Git over HTTP.** `git http-backend` subprocess, PATs over HTTP
   Basic. See [0001](decisions/0001-git-over-http-not-ssh.md). The `body_limit` cap will
   reject large pushes until raised.
2. **Milestone 5 — Repo browsing.** Tree, blob, commit log. **Start with domain value
   objects** — `ObjectId`, `RefName`, `TreeEntry` — before any adapter. A query port
   returning `String`s is an anaemic pass-through that pushes validation into the page.
   Also the point to measure fork/exec cost per page view, and to reconsider `gix` for
   the read path ([0006](decisions/0006-git-binary-behind-narrow-ports.md)).
3. **Milestone 6 — Writing.** Posts, markdown, `/{handle}/posts/{slug}`. Still open
   whether writing or projects/showcases is the better first portfolio feature.

## Open questions

- **Topcoat is early** (v0.5.0, first released 2026-07-22, breaking changes expected
  by its own authors). Expect churn that isn't feature work.
- Body size limits will reject large pushes at Milestone 4 — `topcoat-router` has a
  `body_limit` layer that needs raising on the git routes. Recorded here because it
  will surface as a confusing failure rather than a clear one.
- Topcoat ships Tailwind without Node, which reopens the design system attempt #1
  dropped purely to avoid an npm build step — see [ui.md](ui.md).

## Routing findings (Milestone 0)

- **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat`
  silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local
  stable is now 1.97.1.
- `Router::builder().discover()` collects `#[page]`-annotated items **at link time**,
  so pages can live in any module. Layering is our choice, not the framework's.
- `module_router!` derives each URL from the module tree rather than a path string.
  Still deferred. Application routes now group cleanly (`auth/login`, `api/me`), but
  handles sit at the root ([0004](decisions/0004-root-handles-grouped-routes.md)), so a
  parameterised root segment still has to coexist with static ones. Worth checking how
  `module_router!` handles that before committing to it.
- Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not
  injected as handler arguments. Parses are memoized per request.
- Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's
  `NotFoundError` to render a branded 404.
- `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` is gone.
- `Body` is a boxed `http_body::Body` used for both requests and responses, with
  `into_data_stream()` to read and `Body::new()` to wrap a stream — pack data can
  stream both directions without buffering. This is what makes Milestone 4 viable.
