steid

@jamesgill /

steid/plans/current.md
9.1 KBCode·Blame·Raw
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 5 — Repo browsing
8
9**Goal:** a repository's contents are readable on the web — the file tree at a ref, a
10single file's contents, and the commit log. A visitor can look at code without cloning
11it, which is the first time the profile behaves like a portfolio rather than a list of
12names.
13
14**Out of scope:** editing files, diffs, blame, syntax highlighting, rendering a README as
15markdown (that wants the markdown pipeline Milestone 6 brings), search, and a
16last-commit-per-file column — see Open, where that one is a decision rather than an
17omission.
18
19### Steps
20
21- [ ] Domain: `ObjectId`, `RefName`, `TreeEntry` — value objects **before** any adapter,
22 per [0006]decisions/0006-git-binary-behind-narrow-ports.md
23- [ ] Application: `GitQuery` port — `resolve_ref`, `list_tree`, `read_blob`, `log`
24- [ ] Infrastructure: adapter over the `git` binary, through the existing `git_command`
25- [ ] Application: `browse_repo` read model, reusing `view_repo`'s visibility rule
26- [ ] Web: the tree page, and the repository page showing its default branch
27- [ ] Web: the blob page, and the commit log
28- [ ] Verify in a browser against a real repository, including an empty one
29
30### Done when
31
32A visitor can open a public repository from a profile, see its files at the default
33branch, click into a directory and then a file and read its contents, and open the commit
34log. A private repository shows none of this to someone who may not see it. An empty
35repository says so rather than erroring.
36
37### Settled
38
39- **Value objects come first.** A query port returning `String`s is an anaemic
40 pass-through that pushes validation into the page, which
41 [0006]decisions/0006-git-binary-behind-narrow-ports.md rejected in advance.
42
43- **Fork/exec is ~11–12ms per call, and it is the process, not the query.** Measured on
44 a 201-commit repository, averaged over 50 runs each: `rev-parse` 11.2ms, `ls-tree`
45 11.7ms, `cat-file` 11.6ms, `log -20` 11.8ms, `for-each-ref` 14.4ms. The work is
46 free; starting git is not — which matches the ~13ms `git init --bare` measured in
47 Milestone 3. **A three-call page therefore costs ~35ms of pure overhead.** That is the
48 number the Open decision below turns on, and it is why a per-file last-commit column is
49 a decision and not a detail: at one call per entry, a twenty-file directory is ~230ms
50 before any real work.
51
52### Open
53
54- **What to do about the fork/exec cost.** Three ways, and the measurement above is the
55 input:
56 1. **Keep the binary, design pages to make few calls.** Nothing new to learn, ~35ms a
57 page, and it rules out any per-entry column. Simplest, and honest for a
58 personal-scale forge.
59 2. **`git cat-file --batch`-style long-lived processes.** One process answering many
60 queries over stdin, so the per-call cost mostly disappears without a second
61 implementation of git. Costs a process lifecycle and a protocol to parse.
62 3. **`gix` for the read path**, which 0006 explicitly left open to revisit here.
63 Microseconds instead of milliseconds, and the place `gix` is strongest — but it
64 means two implementations holding assumptions about the same on-disk format, which
65 is the exact thing 0006 rejected it for.
66- **The ref-versus-path ambiguity in the URL.** `/{handle}/repos/{name}/tree/{ref}/{path}`
67 is unparseable in general, because a ref may contain slashes: `tree/feature/x/README`
68 splits two ways. GitHub resolves it by trying candidate splits against the real ref
69 list; GitLab inserts a `/-/` separator. A third option is a single-segment ref with the
70 path after it, refusing refs with slashes. This is a URL shape, so it is expensive to
71 change later.
72- **Whether writing (Milestone 6) should come first.** The ladder puts browsing next, but
73 the vision is portfolio-first and writing is the more distinctive feature, where
74 browsing is the more expected one. Worth deciding before starting rather than after.
75
76### Watch for
77
78- **An empty repository has no `HEAD`.** Steid creates repositories empty, and
79 Milestone 4 made it easy to have one that was never pushed to. `rev-parse HEAD` fails
80 rather than returning nothing, and the repository page must say "nothing here yet"
81 instead of erroring.
82- **A blob is not necessarily text.** Binary files, invalid UTF-8, and very large files
83 all reach the same page. Decide what each does rather than discovering it.
84- **Paths in URLs reach the filesystem indirectly**, via git rather than directly, but a
85 path is still user input arriving at a subprocess argument. `--` before path arguments,
86 as `init_bare` already does.
87- **The commit log is unbounded.** A repository with 50,000 commits needs a limit before
88 the page renders one.
89
90### Carried over — small, unblocked
91
92- **A client that disappears mid-request leaves the body-copy task waiting.** The copy
93 into git's stdin runs in its own task and nothing cancels it if the connection drops.
94 Bounded by the backend exiting and closing the pipe, but not by anything deliberate.
95- **`REMOTE_USER` is not set on the backend**, so a push is recorded in the repository's
96 reflog without naming who made it. Steid knows the actor by then; it simply is not
97 passed through. Small, and worth doing before anything reads reflogs.
98- **No rate limiting on token authentication.** A token is 256 bits so guessing is not
99 the worry; unbounded hashing on an open endpoint is.
100- **Tokens have no expiry and no last-used timestamp.** Both deliberate omissions for
101 now — see [0007]decisions/0007-tokens-over-http-basic.md — but a token list with no
102 "last used" makes it hard to know which are safe to revoke.
103- **A subprocess per git request.** Unlike Milestone 3's once-per-creation, this is on a
104 hot path and has not been measured. Milestone 5 is where that bill comes due.
105- **Streaming is by construction, not by measurement.** The response body is never
106 collected, but no clone large enough to prove it has been run.
107- **An orphaned repo directory is possible** if the process dies between the record
108 write and the filesystem write, and it then blocks re-creating that name. The durable
109 fix is a reconciliation sweep on boot
110 ([architecture.md]architecture.md#db-plus-filesystem-writes); clearing one is a
111 manual `rm` today, since repo deletion does not exist.
112- **The duplicate-name check races.** The loser is caught by `init_bare` or the unique
113 constraint, but surfaces as an opaque storage error rather than "name taken".
114- **Bare repos created on macOS carry `ignorecase = true`.** A migration gotcha if the
115 data directory ever moves to Linux.
116- **Fonts are not loaded.** The theme names Geist and IBM Plex Mono; both fall back
117 today. Topcoat's `font-fontsource` feature handles it.
118- **Light mode is untested.** The palette defines it; nobody has looked at it.
119- **No rate limiting** on `/auth/login` or `/auth/setup`.
120- **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is
121 enforced on read, so this is tidiness, not a hole.
122- **CSRF.** `SameSite=Lax` covers the common case. Forms now exist, so this is decidable
123 rather than hypothetical.
124
125## Backlog
126
127Ordered. Pull from the top.
128
1291. **Milestone 6 — Writing.** Posts, markdown, `/{handle}/posts/{slug}`. Still open
130 whether writing or projects/showcases is the better first portfolio feature.
131
132## Open questions
133
134- **Topcoat is early** (v0.5.0, first released 2026-07-22, breaking changes expected
135 by its own authors). Expect churn that isn't feature work.
136- Topcoat ships Tailwind without Node, which reopens the design system attempt #1
137 dropped purely to avoid an npm build step — see [ui.md]ui.md.
138
139## Routing findings (Milestone 0)
140
141- **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat`
142 silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local
143 stable is now 1.97.1.
144- `Router::builder().discover()` collects `#[page]`-annotated items **at link time**,
145 so pages can live in any module. Layering is our choice, not the framework's.
146- `module_router!` derives each URL from the module tree rather than a path string.
147 Still deferred. Application routes now group cleanly (`auth/login`, `api/me`), but
148 handles sit at the root ([0004]decisions/0004-root-handles-grouped-routes.md), so a
149 parameterised root segment still has to coexist with static ones. Worth checking how
150 `module_router!` handles that before committing to it.
151- Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not
152 injected as handler arguments. Parses are memoized per request.
153- Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's
154 `NotFoundError` to render a branded 404.
155- `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` is gone.
156- `Body` is a boxed `http_body::Body` used for both requests and responses, with
157 `into_data_stream()` to read and `Body::new()` to wrap a stream — pack data can
158 stream both directions without buffering. This is what makes Milestone 4 viable.