# 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`
- [x] 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

- **An orphaned directory is indistinguishable from a duplicate to the visitor.**
  `create_repo` maps `GitStorageError::AlreadyExists` to "that name is taken", which is
  true from outside but hides the inconsistency from the operator. There is no logging
  story yet for it to surface in. The durable fix is the reconciliation sweep in
  [architecture.md](architecture.md#db-plus-filesystem-writes).
- **The duplicate check races.** Two concurrent creates of the same name can both pass
  `find_by_org_and_name`; the loser is then stopped by `init_bare` or, failing that, by
  the `unique (org_id, name)` constraint — which surfaces as an opaque storage error
  rather than "name taken". Correct, just ugly, and single-user for now.

- **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.
