| 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 | |
2eb8681docs: put git next, plan the repo model24d | 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 | |
c6d74a8docs: record the repo model decisions24d | 19 | - [x] Domain: `RepoId`, `RepoName`, `Visibility` (Public/Private), `Repository` |
c5b3ff5feat: repository persistence24d | 20 | - [x] Domain: `RepoRepository` port — `find_by_id`, `find_by_org_and_name`, |
2eb8681docs: put git next, plan the repo model24d | 21 | `list_by_org`, `save` |
c5b3ff5feat: repository persistence24d | 22 | - [x] Infrastructure: in-memory + SQLite implementations, migration |
2eb8681docs: put git next, plan the repo model24d | 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 | |
2eb8681docs: put git next, plan the repo model24d | 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 | |
c6d74a8docs: record the repo model decisions24d | 39 | ### Settled |
2eb8681docs: put git next, plan the repo model24d | 40 | |
c6d74a8docs: record the repo model decisions24d | 41 | - **Repo name rules:** `OrgName`'s, plus `.` and `_` for names like `.github` and |
| 42 | `foo.js`. Lowercased, max 100. Also rejects a name of nothing but dots and any name |
| 43 | ending `.git` — the first is traversal, the second would live at `foo.git.git`. |
| 44 | - **Reserved repo names:** `import`, `new`, `search`. Only names directly under |
| 45 | `/{handle}/repos/` can collide. |
| 46 | - **Visibility defaults to public**, matching a portfolio-first product. |
| 47 | - **Repositories carry an optional description**, capped at 300 characters — a sentence |
| 48 | for the profile listing, not a README. *Added without being asked for; remove if it |
| 49 | is not wanted.* |
c5b3ff5feat: repository persistence24d | 50 | - **`list_by_org` returns every repository regardless of visibility.** Filtering is an |
| 51 | authorization decision and belongs to the use case, so the page and `/api` cannot end |
| 52 | up applying different rules. The cost is that a private repo is briefly in memory |
| 53 | before being filtered, which is fine in-process. |
3954b45docs: record milestone 2 phase 125d | 54 | |
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo | 55 | ### Watch for |
d7b99d9docs: record milestone 0 progress and routing findings1mo | 56 | |
2eb8681docs: put git next, plan the repo model24d | 57 | - **The database and the filesystem cannot share a transaction.** Creating a repo |
| 58 | writes a row and a directory. Neither previous attempt solved this properly — see |
| 59 | [architecture.md](architecture.md#db-plus-filesystem-writes). A compensating delete is |
| 60 | good enough to ship, but write down that an orphaned directory is possible if the |
| 61 | process dies between the two, rather than rediscovering it. |
| 62 | - **Path traversal.** `{data_dir}/{handle}/{name}.git` is built from user input. A name |
| 63 | containing `..` or `/` must be impossible before it reaches the filesystem, and |
| 64 | `RepoName` is the place to make it impossible rather than sanitising at the call site. |
| 65 | - **Visibility is an authorization decision**, so it belongs in the use case. A private |
| 66 | repo must be absent from listings, not merely unlinked — and `/api` must agree with |
| 67 | the page. |
| 68 | - **`git` becomes a runtime dependency** from this milestone. The runbook should say so. |
076dbc9docs: close milestone 1, open milestone 21mo | 69 | |
f0444b7docs: plan milestone 2 in two phases1mo | 70 | ### Carried over — small, unblocked |
076dbc9docs: close milestone 1, open milestone 21mo | 71 | |
2eb8681docs: put git next, plan the repo model24d | 72 | - **Fonts are not loaded.** The theme names Geist and IBM Plex Mono; both fall back |
| 73 | today. Topcoat's `font-fontsource` feature handles it. |
| 74 | - **Light mode is untested.** The palette defines it; nobody has looked at it. |
f0444b7docs: plan milestone 2 in two phases1mo | 75 | - **No rate limiting** on `/auth/login` or `/auth/setup`. |
076dbc9docs: close milestone 1, open milestone 21mo | 76 | - **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is |
| 77 | enforced on read, so this is tidiness, not a hole. |
2eb8681docs: put git next, plan the repo model24d | 78 | - **CSRF.** `SameSite=Lax` covers the common case. Forms now exist, so this is decidable |
| 79 | rather than hypothetical. |
| 80 | |
| 81 | ## Backlog |
| 82 | |
| 83 | Ordered. Pull from the top. |
| 84 | |
2eb8681docs: put git next, plan the repo model24d | 85 | 1. **Milestone 4 — Git over HTTP.** `git http-backend` subprocess, PATs over HTTP |
| 86 | Basic. See [0001](decisions/0001-git-over-http-not-ssh.md). The `body_limit` cap will |
| 87 | reject large pushes until raised. |
| 88 | 2. **Milestone 5 — Repo browsing.** Tree, blob, commit log. |
| 89 | 3. **Milestone 6 — Writing.** Posts, markdown, `/{handle}/posts/{slug}`. Still open |
| 90 | whether writing or projects/showcases is the better first portfolio feature. |
| 91 | |
| 92 | ## Open questions |
| 93 | |
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo | 94 | - **Topcoat is early** (v0.5.0, first released 2026-07-22, breaking changes expected |
| 95 | by its own authors). Expect churn that isn't feature work. |
ca76e1bdocs: fix milestone cross-references after the reorder24d | 96 | - Body size limits will reject large pushes at Milestone 4 — `topcoat-router` has a |
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo | 97 | `body_limit` layer that needs raising on the git routes. Recorded here because it |
| 98 | will surface as a confusing failure rather than a clear one. |
| 99 | - Topcoat ships Tailwind without Node, which reopens the design system attempt #1 |
| 100 | dropped purely to avoid an npm build step — see [ui.md](ui.md). |
| 101 | |
| 102 | ## Routing findings (Milestone 0) |
| 103 | |
| 104 | - **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat` |
| 105 | silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local |
| 106 | stable is now 1.97.1. |
| 107 | - `Router::builder().discover()` collects `#[page]`-annotated items **at link time**, |
| 108 | so pages can live in any module. Layering is our choice, not the framework's. |
| 109 | - `module_router!` derives each URL from the module tree rather than a path string. |
aaefaabfeat: root handles, grouped routes, reserved-handle denylist1mo | 110 | Still deferred. Application routes now group cleanly (`auth/login`, `api/me`), but |
| 111 | handles sit at the root ([0004](decisions/0004-root-handles-grouped-routes.md)), so a |
| 112 | parameterised root segment still has to coexist with static ones. Worth checking how |
| 113 | `module_router!` handles that before committing to it. |
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo | 114 | - Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not |
| 115 | injected as handler arguments. Parses are memoized per request. |
| 116 | - Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's |
| 117 | `NotFoundError` to render a branded 404. |
| 118 | - `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` is gone. |
| 119 | - `Body` is a boxed `http_body::Body` used for both requests and responses, with |
| 120 | `into_data_stream()` to read and `Body::new()` to wrap a stream — pack data can |
ca76e1bdocs: fix milestone cross-references after the reorder24d | 121 | stream both directions without buffering. This is what makes Milestone 4 viable. |