steid

@jamesgill /

steid/plans/current.md
9.3 KBCode·Blame·Raw
ab7fea9chore: plans setup1mo
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
a814db5feat: push and clone private repositories with a token8d
7## Active: Milestone 5 — Repo browsing
8
338102bdocs: plan Milestone 5, with the fork/exec cost measured8d
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.
a814db5feat: push and clone private repositories with a token8d
13
338102bdocs: plan Milestone 5, with the fork/exec cost measured8d
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.
a814db5feat: push and clone private repositories with a token8d
18
338102bdocs: plan Milestone 5, with the fork/exec cost measured8d
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
a814db5feat: push and clone private repositories with a token8d
41 [0006]decisions/0006-git-binary-behind-narrow-ports.md rejected in advance.
338102bdocs: plan Milestone 5, with the fork/exec cost measured8d
42
509ba47docs: settle the Milestone 5 read path on the git binary8d
43- **The read path stays on the `git` binary, one process per query**, with
44 `cat-file --batch` as the named upgrade and `gix` closed off — reasoning, the
45 measurement, and the reopening conditions in
46 [0006]decisions/0006-git-binary-behind-narrow-ports.md#amendment--20260829-the-milestone-5-read-path.
47- **`GitQuery` is a shared handle in app context**, not constructed per request the way
48 the SQLite adapters are. Stage 1 does not need the sharing; stage 2 owns live
49 subprocesses and cannot work without it. The port gives us the seam, not the lifetime,
50 and getting the lifetime wrong now means touching every page later.
51- **No per-file last-commit column in v1.** The direct consequence of the above: at one
52 fork per entry a twenty-file directory is ~230ms. If it is missed, that is the trigger
53 to climb to stage 2 rather than to reopen `gix`.
338102bdocs: plan Milestone 5, with the fork/exec cost measured8d
54- **Fork/exec is ~11–12ms per call, and it is the process, not the query.** Measured on
55 a 201-commit repository, averaged over 50 runs each: `rev-parse` 11.2ms, `ls-tree`
56 11.7ms, `cat-file` 11.6ms, `log -20` 11.8ms, `for-each-ref` 14.4ms. The work is
57 free; starting git is not — which matches the ~13ms `git init --bare` measured in
58 Milestone 3. **A three-call page therefore costs ~35ms of pure overhead.** That is the
59 number the Open decision below turns on, and it is why a per-file last-commit column is
60 a decision and not a detail: at one call per entry, a twenty-file directory is ~230ms
61 before any real work.
8608574feat: personal access tokens, as a domain type8d
62
be4fac5docs: correct the Topcoat guidance in CLAUDE.md24d
63### Open
64
338102bdocs: plan Milestone 5, with the fork/exec cost measured8d
65- **The ref-versus-path ambiguity in the URL.** `/{handle}/repos/{name}/tree/{ref}/{path}`
66 is unparseable in general, because a ref may contain slashes: `tree/feature/x/README`
67 splits two ways. GitHub resolves it by trying candidate splits against the real ref
68 list; GitLab inserts a `/-/` separator. A third option is a single-segment ref with the
69 path after it, refusing refs with slashes. This is a URL shape, so it is expensive to
509ba47docs: settle the Milestone 5 read path on the git binary8d
70 change later. **Recommendation: the separator.** Candidate splits cost a ref lookup —
71 another fork on every page — which the decision above makes more expensive, and a
72 separator is unambiguous by construction rather than by lookup.
73- **How a blob page handles what is not source code.** Binary files, invalid UTF-8, and
74 very large files all arrive at the same page. Deciding beats discovering.
75- **How far back the commit log goes** before it needs paging.
338102bdocs: plan Milestone 5, with the fork/exec cost measured8d
76
77### Watch for
78
79- **An empty repository has no `HEAD`.** Steid creates repositories empty, and
80 Milestone 4 made it easy to have one that was never pushed to. `rev-parse HEAD` fails
81 rather than returning nothing, and the repository page must say "nothing here yet"
82 instead of erroring.
83- **A blob is not necessarily text.** Binary files, invalid UTF-8, and very large files
84 all reach the same page. Decide what each does rather than discovering it.
85- **Paths in URLs reach the filesystem indirectly**, via git rather than directly, but a
86 path is still user input arriving at a subprocess argument. `--` before path arguments,
87 as `init_bare` already does.
88- **The commit log is unbounded.** A repository with 50,000 commits needs a limit before
89 the page renders one.
be4fac5docs: correct the Topcoat guidance in CLAUDE.md24d
90
e0856ebfeat: clone a public repository over HTTP8d
91### Carried over — small, unblocked
d7b99d9docs: record milestone 0 progress and routing findings1mo
92
47db238feat: serve the git protocol through http-backend, behind a port8d
93- **A client that disappears mid-request leaves the body-copy task waiting.** The copy
e0856ebfeat: clone a public repository over HTTP8d
94 into git's stdin runs in its own task and nothing cancels it if the connection drops.
47db238feat: serve the git protocol through http-backend, behind a port8d
95 Bounded by the backend exiting and closing the pipe, but not by anything deliberate.
a814db5feat: push and clone private repositories with a token8d
96- **`REMOTE_USER` is not set on the backend**, so a push is recorded in the repository's
97 reflog without naming who made it. Steid knows the actor by then; it simply is not
98 passed through. Small, and worth doing before anything reads reflogs.
99- **No rate limiting on token authentication.** A token is 256 bits so guessing is not
100 the worry; unbounded hashing on an open endpoint is.
101- **Tokens have no expiry and no last-used timestamp.** Both deliberate omissions for
102 now — see [0007]decisions/0007-tokens-over-http-basic.md — but a token list with no
103 "last used" makes it hard to know which are safe to revoke.
e0856ebfeat: clone a public repository over HTTP8d
104- **A subprocess per git request.** Unlike Milestone 3's once-per-creation, this is on a
105 hot path and has not been measured. Milestone 5 is where that bill comes due.
106- **Streaming is by construction, not by measurement.** The response body is never
107 collected, but no clone large enough to prove it has been run.
6a2a925docs: close Milestone 3, plan Milestone 4a8d
108- **An orphaned repo directory is possible** if the process dies between the record
109 write and the filesystem write, and it then blocks re-creating that name. The durable
110 fix is a reconciliation sweep on boot
111 ([architecture.md]architecture.md#db-plus-filesystem-writes); clearing one is a
112 manual `rm` today, since repo deletion does not exist.
113- **The duplicate-name check races.** The loser is caught by `init_bare` or the unique
114 constraint, but surfaces as an opaque storage error rather than "name taken".
115- **Bare repos created on macOS carry `ignorecase = true`.** A migration gotcha if the
116 data directory ever moves to Linux.
2eb8681docs: put git next, plan the repo model24d
117- **Fonts are not loaded.** The theme names Geist and IBM Plex Mono; both fall back
118 today. Topcoat's `font-fontsource` feature handles it.
119- **Light mode is untested.** The palette defines it; nobody has looked at it.
f0444b7docs: plan milestone 2 in two phases1mo
120- **No rate limiting** on `/auth/login` or `/auth/setup`.
076dbc9docs: close milestone 1, open milestone 21mo
121- **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is
122 enforced on read, so this is tidiness, not a hole.
2eb8681docs: put git next, plan the repo model24d
123- **CSRF.** `SameSite=Lax` covers the common case. Forms now exist, so this is decidable
124 rather than hypothetical.
ab7fea9chore: plans setup1mo
125
126## Backlog
127
128Ordered. Pull from the top.
129
a814db5feat: push and clone private repositories with a token8d
1301. **Milestone 6 — Writing.** Posts, markdown, `/{handle}/posts/{slug}`. Still open
2eb8681docs: put git next, plan the repo model24d
131 whether writing or projects/showcases is the better first portfolio feature.
ab7fea9chore: plans setup1mo
132
133## Open questions
134
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
135- **Topcoat is early** (v0.5.0, first released 2026-07-22, breaking changes expected
136 by its own authors). Expect churn that isn't feature work.
137- Topcoat ships Tailwind without Node, which reopens the design system attempt #1
138 dropped purely to avoid an npm build step — see [ui.md]ui.md.
139
140## Routing findings (Milestone 0)
141
142- **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat`
143 silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local
144 stable is now 1.97.1.
145- `Router::builder().discover()` collects `#[page]`-annotated items **at link time**,
146 so pages can live in any module. Layering is our choice, not the framework's.
147- `module_router!` derives each URL from the module tree rather than a path string.
aaefaabfeat: root handles, grouped routes, reserved-handle denylist1mo
148 Still deferred. Application routes now group cleanly (`auth/login`, `api/me`), but
149 handles sit at the root ([0004]decisions/0004-root-handles-grouped-routes.md), so a
150 parameterised root segment still has to coexist with static ones. Worth checking how
151 `module_router!` handles that before committing to it.
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
152- Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not
153 injected as handler arguments. Parses are memoized per request.
154- Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's
155 `NotFoundError` to render a branded 404.
156- `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` is gone.
157- `Body` is a boxed `http_body::Body` used for both requests and responses, with
158 `into_data_stream()` to read and `Body::new()` to wrap a stream — pack data can
ca76e1bdocs: fix milestone cross-references after the reorder24d
159 stream both directions without buffering. This is what makes Milestone 4 viable.