| 1 | # Current |
| 2 | |
| 3 | > Keep this file short. One active step, one ordered backlog. Completed work moves to |
| 4 | > [progress.md](progress.md). If this file starts reading like a changelog, it has |
| 5 | > drifted — that's exactly what went wrong last time. |
| 6 | |
| 7 | ## Active: Milestone 3 — Repo model |
| 8 | |
| 9 | **Goal:** repositories exist as records and as bare git repos on disk, and they appear |
| 10 | on the profile. No git protocol yet — that is milestone 4. This milestone fills the |
| 11 | Repositories section and gets the storage layout right before anything serves it. |
| 12 | |
| 13 | **Out of scope:** clone, push, browsing a tree, README rendering, forks, stars. |
| 14 | Deleting a repo — worth having, but it makes the filesystem/database consistency |
| 15 | problem twice as interesting, so not in the first pass. |
| 16 | |
| 17 | ### Steps |
| 18 | |
| 19 | - [ ] Domain: `RepoId`, `RepoName`, `Visibility` (Public/Private), `Repository` |
| 20 | - [ ] Domain: `RepoRepository` port — `find_by_id`, `find_by_org_and_name`, |
| 21 | `list_by_org`, `save` |
| 22 | - [ ] Infrastructure: in-memory + SQLite implementations, migration |
| 23 | - [ ] Application: `GitStorage` port — `init_bare`, `repo_path` |
| 24 | - [ ] Infrastructure: `DiskGitStorage`, shelling out to `git init --bare` |
| 25 | - [ ] Application: `create_repo` use case — owner only, validates, creates record and |
| 26 | bare repo |
| 27 | - [ ] Application: `list_repos` / `view_repo` read models — visibility-aware |
| 28 | - [ ] Web: `/{handle}/repos/new` form, `/{handle}/repos/{name}` page |
| 29 | - [ ] Web: the profile's Repositories section lists what the viewer may see |
| 30 | - [ ] `/api/users/{handle}/repos` |
| 31 | |
| 32 | ### Done when |
| 33 | |
| 34 | The owner creates a repo through the UI, a bare repo appears at |
| 35 | `{data_dir}/{handle}/{name}.git`, and it is listed on the profile. A private repo is |
| 36 | invisible to a signed-out visitor. `git clone` does **not** work yet — that is |
| 37 | milestone 4. |
| 38 | |
| 39 | ### Decide during |
| 40 | |
| 41 | - **Repo name rules.** `OrgName` allows `[a-z0-9-]` and lowercases. Repo names |
| 42 | conventionally allow dots and underscores (`.github`, `my_repo`, `foo.js`) and are |
| 43 | often case-preserving. Following `OrgName` exactly is simplest and rejects names |
| 44 | people will reasonably want; allowing more means deciding about case-insensitive |
| 45 | uniqueness and about names that are awkward on disk. |
| 46 | - **`new` collides.** `/{handle}/repos/new` is a static route and static beats |
| 47 | parameterised, so a repo actually named `new` would be unreachable at its own URL. |
| 48 | Same problem as handles, same fix: a small reserved list on `RepoName`. Reserve |
| 49 | before the first repo exists. |
| 50 | - **Visibility default.** Public matches a portfolio-first product; private matches |
| 51 | every forge people are used to. |
| 52 | |
| 53 | ### Watch for |
| 54 | |
| 55 | - **The database and the filesystem cannot share a transaction.** Creating a repo |
| 56 | writes a row and a directory. Neither previous attempt solved this properly — see |
| 57 | [architecture.md](architecture.md#db-plus-filesystem-writes). A compensating delete is |
| 58 | good enough to ship, but write down that an orphaned directory is possible if the |
| 59 | process dies between the two, rather than rediscovering it. |
| 60 | - **Path traversal.** `{data_dir}/{handle}/{name}.git` is built from user input. A name |
| 61 | containing `..` or `/` must be impossible before it reaches the filesystem, and |
| 62 | `RepoName` is the place to make it impossible rather than sanitising at the call site. |
| 63 | - **Visibility is an authorization decision**, so it belongs in the use case. A private |
| 64 | repo must be absent from listings, not merely unlinked — and `/api` must agree with |
| 65 | the page. |
| 66 | - **`git` becomes a runtime dependency** from this milestone. The runbook should say so. |
| 67 | |
| 68 | ### Carried over — small, unblocked |
| 69 | |
| 70 | - **Fonts are not loaded.** The theme names Geist and IBM Plex Mono; both fall back |
| 71 | today. Topcoat's `font-fontsource` feature handles it. |
| 72 | - **Light mode is untested.** The palette defines it; nobody has looked at it. |
| 73 | - **No rate limiting** on `/auth/login` or `/auth/setup`. |
| 74 | - **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is |
| 75 | enforced on read, so this is tidiness, not a hole. |
| 76 | - **CSRF.** `SameSite=Lax` covers the common case. Forms now exist, so this is decidable |
| 77 | rather than hypothetical. |
| 78 | |
| 79 | ## Backlog |
| 80 | |
| 81 | Ordered. Pull from the top. |
| 82 | |
| 83 | 1. **Milestone 4 — Git over HTTP.** `git http-backend` subprocess, PATs over HTTP |
| 84 | Basic. See [0001](decisions/0001-git-over-http-not-ssh.md). The `body_limit` cap will |
| 85 | reject large pushes until raised. |
| 86 | 2. **Milestone 5 — Repo browsing.** Tree, blob, commit log. |
| 87 | 3. **Milestone 6 — Writing.** Posts, markdown, `/{handle}/posts/{slug}`. Still open |
| 88 | whether writing or projects/showcases is the better first portfolio feature. |
| 89 | |
| 90 | ## Open questions |
| 91 | |
| 92 | - **Topcoat is early** (v0.5.0, first released 2026-07-22, breaking changes expected |
| 93 | by its own authors). Expect churn that isn't feature work. |
| 94 | - Body size limits will reject large pushes at Milestone 5 — `topcoat-router` has a |
| 95 | `body_limit` layer that needs raising on the git routes. Recorded here because it |
| 96 | will surface as a confusing failure rather than a clear one. |
| 97 | - Topcoat ships Tailwind without Node, which reopens the design system attempt #1 |
| 98 | dropped purely to avoid an npm build step — see [ui.md](ui.md). |
| 99 | |
| 100 | ## Routing findings (Milestone 0) |
| 101 | |
| 102 | - **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat` |
| 103 | silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local |
| 104 | stable is now 1.97.1. |
| 105 | - `Router::builder().discover()` collects `#[page]`-annotated items **at link time**, |
| 106 | so pages can live in any module. Layering is our choice, not the framework's. |
| 107 | - `module_router!` derives each URL from the module tree rather than a path string. |
| 108 | Still deferred. Application routes now group cleanly (`auth/login`, `api/me`), but |
| 109 | handles sit at the root ([0004](decisions/0004-root-handles-grouped-routes.md)), so a |
| 110 | parameterised root segment still has to coexist with static ones. Worth checking how |
| 111 | `module_router!` handles that before committing to it. |
| 112 | - Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not |
| 113 | injected as handler arguments. Parses are memoized per request. |
| 114 | - Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's |
| 115 | `NotFoundError` to render a branded 404. |
| 116 | - `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` is gone. |
| 117 | - `Body` is a boxed `http_body::Body` used for both requests and responses, with |
| 118 | `into_data_stream()` to read and `Body::new()` to wrap a stream — pack data can |
| 119 | stream both directions without buffering. This is what makes Milestone 5 viable. |