steid

@jamesgill /

steid/plans/current.md
9.1 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
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.
8608574feat: personal access tokens, as a domain type8d
51
be4fac5docs: correct the Topcoat guidance in CLAUDE.md24d
52### Open
53
338102bdocs: plan Milestone 5, with the fork/exec cost measured8d
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.
a814db5feat: push and clone private repositories with a token8d
72- **Whether writing (Milestone 6) should come first.** The ladder puts browsing next, but
338102bdocs: plan Milestone 5, with the fork/exec cost measured8d
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.
be4fac5docs: correct the Topcoat guidance in CLAUDE.md24d
89
e0856ebfeat: clone a public repository over HTTP8d
90### Carried over — small, unblocked
d7b99d9docs: record milestone 0 progress and routing findings1mo
91
47db238feat: serve the git protocol through http-backend, behind a port8d
92- **A client that disappears mid-request leaves the body-copy task waiting.** The copy
e0856ebfeat: clone a public repository over HTTP8d
93 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
94 Bounded by the backend exiting and closing the pipe, but not by anything deliberate.
a814db5feat: push and clone private repositories with a token8d
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.
e0856ebfeat: clone a public repository over HTTP8d
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.
6a2a925docs: close Milestone 3, plan Milestone 4a8d
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.
2eb8681docs: put git next, plan the repo model24d
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.
f0444b7docs: plan milestone 2 in two phases1mo
119- **No rate limiting** on `/auth/login` or `/auth/setup`.
076dbc9docs: close milestone 1, open milestone 21mo
120- **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is
121 enforced on read, so this is tidiness, not a hole.
2eb8681docs: put git next, plan the repo model24d
122- **CSRF.** `SameSite=Lax` covers the common case. Forms now exist, so this is decidable
123 rather than hypothetical.
ab7fea9chore: plans setup1mo
124
125## Backlog
126
127Ordered. Pull from the top.
128
a814db5feat: push and clone private repositories with a token8d
1291. **Milestone 6 — Writing.** Posts, markdown, `/{handle}/posts/{slug}`. Still open
2eb8681docs: put git next, plan the repo model24d
130 whether writing or projects/showcases is the better first portfolio feature.
ab7fea9chore: plans setup1mo
131
132## Open questions
133
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
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.
aaefaabfeat: root handles, grouped routes, reserved-handle denylist1mo
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.
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
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
ca76e1bdocs: fix milestone cross-references after the reorder24d
158 stream both directions without buffering. This is what makes Milestone 4 viable.