steid

@jamesgill /

docs: serve git over smart HTTP, reorder roadmap portfolio-first

Decision 0001: git moves to smart HTTP via git http-backend with PATs over
HTTP Basic, replacing embedded SSH. This removes the only context-less
transport, so the layering question that was blocking Milestone 1 stops
existing rather than needing an answer. It also folds SSH key management
into tokens, which /api needs anyway. Verified Body streams both directions
before committing to it.

Roadmap reordered so the differentiator ships first. The previous ladder was
attempt #2's renumbered: it declared portfolio-first, then put portfolio at
milestone 6 of 8 behind four milestones of git plumbing. Two attempts and
~6,000 lines have produced no portfolio feature at all. Profile and writing
now come before repos; full multi-user identity defers to 7.

/api becomes a standing practice rather than a milestone -- a third consumer
keeps the use case layer honest.

Architecture states a position on data access instead of posing a question:
writes and authorization through use cases, reads may go direct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JamesPatrickGill authored 1 month agoparentd7b99d9Browse filesbd48b4bd670786c0a53694000c12d81d3cf91965

5 files changed+215 −126

plans/ROADMAP.md+42 −24View file
@@ -30,40 +30,58 @@ thing that appears on it. That framing should win any tie-break.
3030 Starting intent, not settled decisions. Each one gets a record in
3131 [decisions/](decisions/) when it's actually made and confirmed in this build.
3232
33| Concern | Intent |
34|---|---|
35| Web framework | Topcoat (tokio-rs) |
36| Git protocol | shell out to the `git` binary |
37| SSH transport | embedded russh server |
38| Crate layout | single crate |
39| Ownership | personal org owns repos |
40| Database | SQLite via sqlx |
33+| Concern | Choice | Decision |
34+|---|---|---|
35+| Web framework | Topcoat (tokio-rs) | — |
36+| Git transport | smart HTTP via `git http-backend` | [0001](decisions/0001-git-over-http-not-ssh.md) |
37+| Git auth | personal access tokens over HTTP Basic | [0001](decisions/0001-git-over-http-not-ssh.md) |
38+| Crate layout | single crate | — |
39+| Ownership | personal org owns repos | — |
40+| Database | SQLite via sqlx | — |
4141
4242 Architecture and conventions: [architecture.md](architecture.md).
4343
4444 ## Milestone Ladder
4545
46This repo is a from-scratch rebuild on Topcoat. Milestones 1–4 were built and
47verified end-to-end in a previous attempt — see [progress.md](progress.md) for what
48that proved. Treat that code as reference, not as a baseline.
46+A from-scratch rebuild on Topcoat. Much of this was built and verified end-to-end in a
47+previous attempt — see [progress.md](progress.md). Treat that code as reference, not as
48+a baseline.
4949
5050 | # | Milestone | Status |
5151 |---|---|---|
52| 0 | **Skeleton** — Topcoat app boots, config, one page, SQLite wired | nearly done |
53| 1 | **Identity** — users, orgs, memberships, sessions, registration policy | not started |
54| 2 | **Repo model** — repo records + bare repos on disk | not started |
55| 3 | **Git over SSH** — clone and push via embedded russh | not started |
56| 4 | **SSH key auth + authz** — pubkey → user, membership-gated push/clone | not started |
57| 5 | **Repo browsing** — tree, blob, commit log in the web UI | not started |
58| 6 | **Portfolio** — blogs, pages, project showcases | not started |
59| 7 | **Collaboration** — issues, PRs, review | not started |
60| 8 | **API / federation** | not started |
61
62Milestones 5–8 have never been built. Everything below 5 has a working reference
63implementation to crib from.
52+| 0 | **Skeleton** — Topcoat app boots, config, one page, SQLite wired | done |
53+| 1 | **Identity, thin** — owner from config, login, session | active |
54+| 2 | **Profile page** — `/{owner}` as the real home page | not started |
55+| 3 | **Writing** — posts, markdown | not started |
56+| 4 | **Repo model** — records + bare repos on disk | not started |
57+| 5 | **Git over HTTP** — `git http-backend`, PATs for auth | not started |
58+| 6 | **Repo browsing** — tree, blob, commit log | not started |
59+| 7 | **Identity, full** — multi-user, orgs, invites, registration policy | not started |
60+| 8+ | Projects/showcases · issues & PRs · SSH transport · federation | not started |
61+
62+`/api` is not a milestone. It's a standing practice: every milestone that adds a use
63+case exposes it under `/api` where that makes sense. An API isn't a feature, it's a
64+second surface on work already being done — and having a third consumer keeps the use
65+case layer honest.
66+
67+### Why this order
68+
69+The vision calls Steid portfolio-first, but every previous ladder put portfolio behind
70+four milestones of git plumbing. Across two attempts and ~6,000 lines, Steid has
71+shipped identity, repo records, clone, push, and key auth — and not one portfolio
72+feature. Attempt #2 ended with all the git infrastructure working and nothing that
73+distinguishes it from Gitea.
74+
75+So the differentiator ships early (2–3), and git — which is well-understood and has a
76+working reference implementation — comes after. Full multi-user identity is deferred
77+to 7, because personal-first means one user, and there's nothing to collaborate on
78+until there's something worth collaborating on.
79+
80+**Open:** milestone 3 is written as *writing/posts*, but *projects & showcases* is the
81+other candidate for the first portfolio feature. Unresolved.
6482
6583 **One numbering scheme.** The previous attempt kept two (a roadmap ladder and a
6684 separate build log with conflicting numbers) and they drifted apart within three
6785 milestones. If the plan changes, edit this table — don't start a second list.
6886
69Milestone 0 is broken into steps in [current.md](current.md).
87+The active milestone is broken into steps in [current.md](current.md).
plans/architecture.md+23 −21View file
@@ -84,33 +84,35 @@ enough to ship; write down that it's a known hole rather than rediscovering it.
8484 durable fix is a reconciliation sweep on boot, or marking rows pending and committing
8585 after the filesystem write lands.
8686
87## Open question: Topcoat's data access vs clean architecture
87+## Data access
8888
89**This is the one genuinely unresolved design question, and it blocks Milestone 1.**
90
91Topcoat's model is async components that query the database directly and check
92permissions inline, with `#[memoize]` deduplicating calls per request. State comes
93from `app_context::<T>(cx)`, and the framework's own guidance is to "prefer composable
89+Topcoat's grain is async components that query the database directly and check
90+permissions inline, with `#[memoize]` deduplicating calls per request. State comes from
91+`app_context::<T>(cx)`, and the framework's own guidance is to "prefer composable
9492 `cx: &Cx` functions over middleware/extractors for auth and request-scoped data."
9593
96That is a view layer reaching straight for the data layer — the exact thing the layer
97rules above forbid. The two models are in real tension and the resolution has to be
98deliberate:
94+That reads as a view layer reaching for the data layer, which the rules above forbid.
95+It was a genuinely hard question while SSH was the git transport, because an SSH
96+channel handler has no `Cx` — so authorization expressed in a component was invisible
97+to `serve_push`, and the two had to share something.
98+
99+[Decision 0001](decisions/0001-git-over-http-not-ssh.md) removed that transport. Every
100+caller now has a `Cx`, so nothing *forces* a transport-neutral use case layer. The
101+position is therefore a choice, taken deliberately:
102+
103+**Writes and authorization go through use cases. Reads may go direct.**
99104
100- **Hold the line.** Components call use cases; use cases hold the ports. Costs some
101 of Topcoat's ergonomics and may fight the framework's grain.
102- **Let components read, force writes through use cases.** Reads go direct (they're
103 the ones that benefit from memoization and per-component fetching); every mutation
104 and every authorization decision stays in a use case. Pragmatic middle.
105- **Adopt Topcoat's model fully.** Fastest to build, and abandons the layering that
106 made the previous attempt's authorization work uniformly across web *and* SSH.
105+- Any mutation, and any decision about what an actor is allowed to do, lives in an
106+ application use case that takes an `Actor` and the ports it needs. One place, one
107+ answer, reachable from a page, an `/api` route, or a future SSH adapter alike.
108+- Straightforward reads for display may query through the pool in a component, which
109+ is where memoization and per-component fetching earn their keep.
107110
108The SSH transport is the thing that makes this non-obvious: `serve_push` has no `Cx`
109and no request. Whatever authorization lives in a Topcoat component is unavailable to
110it. Anything enforcing a permission needs to sit somewhere both transports can reach.
111+The tell for when a read has outgrown that: if it starts deciding whether the viewer is
112+allowed to see something, it isn't a read any more — move it.
111113
112Decide before Milestone 1 and write it up as decision 0001 — see
113[decisions/TEMPLATE.md](decisions/TEMPLATE.md).
114+Revisit if `/api` and the pages start duplicating query logic. That's the signal the
115+line is in the wrong place.
114116
115117 ## Safety
116118
plans/current.md+64 −55View file
@@ -4,75 +4,84 @@
44 > [progress.md](progress.md). If this file starts reading like a changelog, it has
55 > drifted — that's exactly what went wrong last time.
66
7## Active: Milestone 0 — Skeleton
7+## Active: Milestone 1 — Identity, thin
88
9**Goal:** a Topcoat app that boots, serves one page, reads config from env, and opens
10a SQLite pool. No domain logic yet. The point is to learn Topcoat's shape before
11committing the architecture to it.
9+**Goal:** the app knows who you are. One owner, bootstrapped from config, who can log
10+in and out. Enough identity to hang a profile page off, and no more.
11+
12+**Explicitly out of scope** — these are Milestone 7: multi-user registration,
13+invite codes, `RegistrationPolicy`, organisation management UI, roles beyond owner.
1214
1315 ### Steps
1416
15- [x] Add `topcoat` 0.5 + `tokio` to `Cargo.toml`
16- [x] Get the getting-started hello-world page rendering
17- [x] Work out how routing wants the source tree laid out — see
18 [routing findings](#routing-findings) below
19- [x] Config from env via `envy` (`STEID_*`) — see [runbook.md](runbook.md)
20- [x] SQLite pool registered as app context; page reads it via `app_context::<T>(cx)`
21- [ ] Install `topcoat-cli` and confirm `topcoat dev` watch/reload works
22- [ ] Decide the layering question in [architecture.md](architecture.md#open-question-topcoats-data-access-vs-clean-architecture)
23 — this is the one genuinely open design question and it blocks Milestone 1
17+- [ ] Domain: typed IDs, `Email`, `PasswordHash`, `User`, `Organization`,
18+ `Membership`, `Role`, `Actor`, `DomainError`
19+- [ ] Domain: repository traits — `UserRepository`, `OrgRepository`,
20+ `MembershipRepository`
21+- [ ] Infrastructure: in-memory implementations (these are what make use cases
22+ testable without a database)
23+- [ ] Infrastructure: migrations `001`–`003`, SQLite implementations
24+- [ ] Application: `PasswordHasher` port + Argon2 adapter, stub hasher for tests
25+- [ ] Application: `bootstrap_owner` use case — creates org, then user, then
26+ membership, idempotent on reboot
27+- [ ] Application: `login` use case — verifies credentials, returns an `Actor`
28+- [ ] Web: login page, logout, session cookie, `current_actor(cx)` helper
29+- [ ] `/api/me` — first `/api` route, proves the use case layer has two consumers
2430
2531 ### Done when
2632
27`cargo run` serves a page that renders a value read from SQLite, with config supplied
28by env. **Met** — renders the SQLite version, and `STEID_DATABASE_URL` redirects it to
29a different file.
33+A fresh database boots into an owner account from `STEID_OWNER_*`; logging in through
34+the web UI sets a session; `/api/me` returns that identity; logging out clears it.
3035
31### Routing findings
36+### Watch for
3237
33- **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat`
34 silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local
35 stable is now 1.97.1. Worth a `rust-toolchain.toml` if this ever builds on another
36 machine.
37- `Router::builder().discover()` collects `#[page]`-annotated items **at link time**,
38 so pages can live in any module. Layering is our choice, not the framework's — the
39 home page sits in `infrastructure/web.rs` per `architecture.md`.
40- `module_router!` is Topcoat's recommended alternative, deriving each URL from the
41 module tree rather than a path string. **Deferred to Milestone 1.** Steid's URL space
42 is parameterised at the root (`/{owner}`, `/{owner}/{repo}`, `/{owner}/keys`), which
43 means `path_param!` declarations inside route modules; worth designing once there are
44 real routes rather than guessing now.
45- Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not
46 injected as handler arguments. Parses are memoized per request.
47- Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's
48 `NotFoundError` to render a branded 404 — that replaces attempt #2's `not_found.html`.
49- `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` from attempt #2 is
50 gone. `runbook.md` reflects this.
38+- **Foreign key ordering.** The org must be saved before the user — attempt #2 had to
39+ fix this in both `bootstrap_owner` and `register_user`. See
40+ [progress.md](progress.md#identity).
41+- **Bootstrap must be idempotent.** It runs on every boot, not just the first.
42+- Never log or `Debug`-print a password. `PasswordHash` is opaque on purpose.
5143
5244 ## Backlog
5345
5446 Ordered. Pull from the top.
5547
561. **Milestone 1 — Identity.** Domain model (User, Org, Membership, Actor, Role),
57 value objects (Email, PasswordHash), typed IDs, repository ports, Argon2 hashing,
58 registration policy, session cookies. Reference implementation exists and was
59 solid — port the domain layer, rewrite the web layer.
602. **Milestone 2 — Repo model.** `Repository` entity, `Visibility`, `create_repo` use
61 case, bare repo on disk at `{data_dir}/{org}/{repo}.git`. Watch the
62 DB-plus-filesystem atomicity problem — see
63 [architecture.md](architecture.md#db-plus-filesystem-writes).
643. **Milestone 3 — Git over SSH.** `GitProtocolServer` port, `GitBinary` adapter,
65 embedded russh. Channel-splitting is fiddly; the notes in
66 [progress.md](progress.md#ssh-channel-bridging) are hard-won.
674. **Milestone 4 — SSH key auth + authz.**
48+1. **Milestone 2 — Profile page.** `/{owner}` becomes the real home page, replacing
49+ the Milestone 0 placeholder. The frame the rest of the product hangs in.
50+2. **Milestone 3 — Writing.** Posts, markdown rendering, `/{owner}/{slug}`.
51+ *Open question: is writing actually the first portfolio feature, or is it
52+ projects/showcases?*
53+3. **Milestone 4 — Repo model.** `Repository` entity, `Visibility`, `create_repo`,
54+ bare repo on disk at `{data_dir}/{org}/{repo}.git`. Watch the DB-plus-filesystem
55+ atomicity problem — see [architecture.md](architecture.md#db-plus-filesystem-writes).
56+4. **Milestone 5 — Git over HTTP.** `git http-backend` subprocess, PATs over HTTP
57+ Basic. See [0001](decisions/0001-git-over-http-not-ssh.md).
6858
6959 ## Open questions
7060
71- **Topcoat is nine days old** (v0.5.0, first release 2026-07-22, breaking changes
72 expected). Pin the exact version and expect to chase it. Budget time for churn that
73 isn't feature work.
74- Does Topcoat's asset/CSS pipeline coexist with an embedded russh server in one
75 binary, or does the CLI's watch-and-rebuild model push SSH into a separate process?
76 Unresolved — affects the embedded-SSH approach and single-binary deploy.
77- Topcoat ships Tailwind without Node. That reopens the design system from attempt #1
78 that was dropped purely to avoid an npm build step — see [ui.md](ui.md).
61+- **Topcoat is early** (v0.5.0, first released 2026-07-22, breaking changes expected
62+ by its own authors). Expect churn that isn't feature work.
63+- Body size limits will reject large pushes at Milestone 5 — `topcoat-router` has a
64+ `body_limit` layer that needs raising on the git routes. Recorded here because it
65+ will surface as a confusing failure rather than a clear one.
66+- Topcoat ships Tailwind without Node, which reopens the design system attempt #1
67+ dropped purely to avoid an npm build step — see [ui.md](ui.md).
68+
69+## Routing findings (Milestone 0)
70+
71+- **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat`
72+ silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local
73+ stable is now 1.97.1.
74+- `Router::builder().discover()` collects `#[page]`-annotated items **at link time**,
75+ so pages can live in any module. Layering is our choice, not the framework's.
76+- `module_router!` derives each URL from the module tree rather than a path string.
77+ Still deferred — Steid's URL space is parameterised at the root (`/{owner}`,
78+ `/{owner}/{repo}`), which means `path_param!` declarations inside route modules.
79+ Worth designing at Milestone 2 when the profile page makes it concrete.
80+- Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not
81+ injected as handler arguments. Parses are memoized per request.
82+- Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's
83+ `NotFoundError` to render a branded 404.
84+- `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` is gone.
85+- `Body` is a boxed `http_body::Body` used for both requests and responses, with
86+ `into_data_stream()` to read and `Body::new()` to wrap a stream — pack data can
87+ stream both directions without buffering. This is what makes Milestone 5 viable.
plans/decisions/0001-git-over-http-not-ssh.md+69 −0View file
@@ -0,0 +1,69 @@
1+# 0001 — Serve git over smart HTTP, not SSH
2+
3+**Status:** accepted · **Date:** 2026-07-31
4+
5+## Context
6+
7+The previous attempt served git over an embedded russh server and reached working
8+clone, push, and public-key authentication. Carrying that forward raised three
9+problems at once:
10+
11+1. **Unknown coexistence risk.** Topcoat's `topcoat dev` builds, watches sources, and
12+ restarts the app. Whether a long-lived embedded SSH listener survives that loop was
13+ untested, and it sat under four milestones of planned work.
14+2. **A layering question with no cheap answer.** Topcoat's grain is async components
15+ that query the database and check permissions inline. An SSH channel handler has no
16+ `Cx`, no request, and no cookies, so any authorization expressed in a component is
17+ invisible to `serve_push`. Reconciling the two needed a deliberate architectural
18+ position before any domain code could be written.
19+3. **A whole subsystem for key management.** SSH public keys need a table, SHA256
20+ fingerprinting, canonical re-encoding, dedup, and revocation UI — roughly a
21+ milestone of work serving one transport.
22+
23+Git's smart HTTP protocol is an alternative transport with the same capabilities.
24+Notably, the original roadmap called for HTTP first; SSH was an unplanned detour.
25+
26+## Decision
27+
28+Serve git over smart HTTP as the primary transport, delegating the protocol to
29+`git http-backend` — the CGI that ships with git and implements `info/refs`,
30+`upload-pack`, `receive-pack`, and dumb-protocol fallback.
31+
32+Authenticate with personal access tokens over HTTP Basic. SSH may return later as an
33+optional transport; it is not load-bearing.
34+
35+Verified before accepting: `topcoat_router::Body` is a boxed `http_body::Body` used
36+for both requests and responses, with `into_data_stream()` for reading and
37+`Body::new()` wrapping any stream for writing — so pack data streams in and out
38+without buffering.
39+
40+## Alternatives considered
41+
42+- **Embedded SSH (russh), as attempt #2 did.** Better developer experience — key-based
43+ auth with no credential prompts. Rejected for the three costs above; the deciding
44+ factor was that it was the only context-less transport, and removing it dissolves an
45+ architectural question rather than answering it.
46+- **Implement smart HTTP directly.** More control, no subprocess. Rejected for the same
47+ reason gitoxide was rejected: protocol details we would own and get wrong, against a
48+ binary that already handles every case correctly.
49+- **Both transports from the start.** Twice the surface before the product exists.
50+
51+## Consequences
52+
53+- **The layering question disappears.** Every transport now has a `Cx`. There is no
54+ context-less caller, so a transport-neutral use-case layer is a matter of taste
55+ rather than necessity. See [architecture.md](../architecture.md#data-access).
56+- **No russh, no host keys, no coexistence risk.** One protocol, one port, and TLS
57+ terminates the way it does for any web app.
58+- **SSH key management collapses into personal access tokens** — smaller, and needed
59+ for `/api` regardless, so the work is shared rather than additional.
60+- **Worse developer experience, chosen knowingly.** `git clone https://…` prompts for
61+ credentials unless a credential helper is configured. SSH keys are nicer. GitHub ran
62+ HTTPS-plus-token as its primary path for years, so the path is well-trodden, but this
63+ is a real regression against attempt #2.
64+- **`git` remains a runtime dependency**, now including `git-http-backend`.
65+- **Body size limits will bite.** `topcoat-router` has a `body_limit` layer; a large
66+ push will be rejected until the cap is raised on the git routes. Expect to hit this
67+ as a confusing failure rather than a clear one.
68+- Reversible. `GitProtocolServer` stays a port, so adding SSH later means an adapter
69+ plus a key-management subsystem — not a rewrite.
plans/runbook.md+17 −26View file
@@ -25,12 +25,11 @@ cargo test
2525
2626 ```bash
2727 cargo install topcoat-cli # dev server: watch, rebuild, asset bundling
28topcoat dev # not yet tried on this project
28+topcoat dev # working
2929 ```
3030
3131 `topcoat dev` builds, bundles assets, watches sources, and live-reloads pages that
32include `topcoat::dev::script()`. How that interacts with an embedded SSH server is
33unresolved — see [current.md](current.md#open-questions).
32+include `topcoat::dev::script()`. Press `r` to force a rebuild.
3433
3534 ## Configuration
3635
@@ -68,24 +67,23 @@ boot. Keep `.env` and `.env.prod` out of git — both are gitignored.
6867
6968 Bare repos at `{STEID_DATA_DIR}/{org}/{repo}.git`. Created empty — no initial commit.
7069
71## SSH (#2)
70+## Git transport (Milestone 5)
7271
73The SSH server is embedded (russh), not OpenSSH — there is no `authorized_keys`
74configuration and no forced command. Users register public keys through the web UI at
75`/{owner}/keys`, and the server matches incoming keys by SHA256 fingerprint.
76
77Host key generation and persistence was never written down. Sort it out during
78Milestone 3 and document it here — a host key regenerated on each boot means every
79client gets a changed-host-key warning.
72+Smart HTTP, delegated to `git http-backend`, authenticated with personal access tokens
73+over HTTP Basic — see [0001](decisions/0001-git-over-http-not-ssh.md). No SSH, no host
74+keys, no `authorized_keys`.
8075
8176 ```bash
82git clone git@host:owner/repo
77+git clone http://host/owner/repo.git
8378 ```
8479
80+Fill in the token workflow and the `body_limit` setting once this is built.
81+
8582 ## Manual verification checklist (#2)
8683
8784 Attempt #2 verified these by hand each milestone but never wrote down the steps. They
88are the smoke test for Milestones 2–4:
85+are the smoke test for Milestones 4–5. The auth rows assumed SSH keys; the shape of
86+the check still holds with tokens substituted:
8987
9088 - [ ] Create a repo via the web UI → bare repo appears at
9189 `{data_dir}/{org}/{repo}.git`
@@ -94,25 +92,18 @@ are the smoke test for Milestones 2–4:
9492 - [ ] `git clone` a non-existent repo → clean error, not a hang or panic
9593 - [ ] First push to an empty repo → succeeds
9694 - [ ] Push to a repo with history → succeeds
97- [ ] Clone with an unregistered key → `Permission denied`, exit 128
98- [ ] Register key via `/{owner}/keys` → clone and push both succeed
99- [ ] Revoke key via web UI → subsequent clone rejected at auth
95+- [ ] Push a repo large enough to exercise the `body_limit` cap → succeeds
96+- [ ] Clone with no credentials → rejected, and the prompt is comprehensible
97+- [ ] Clone with a valid token → succeeds
98+- [ ] Revoke the token → subsequent clone rejected at auth
10099 - [ ] Clone a private repo as a non-member → rejected
101100 - [ ] Push as a non-owner member → rejected
102101
103102 Worth automating as an integration test rather than re-running by hand a fourth time.
104103
105## Suggested `.gitignore` additions
106
107Not applied yet — no code to ignore. When the app lands:
104+## `.gitignore`
108105
109```
110/data
111*.db
112*.db-shm
113*.db-wal
114.env.prod
115```
106+Applied: `/target`, `/data`, `*.db*`, `.env`, `.env.prod`.
116107
117108 **Do not add `/plans`.** Attempt #2 did, and that is why these docs had to be
118109 hand-carried between repos.