# PLAN-layering.md — One server, two expressions
Cross-repo: commits are tagged **[serve]** or **[static]** or **[unified]** for the
repository they land in. Version bumps follow each repo's own convention.
## Where We Are
Two independent HTTP servers in one ecosystem.
`mini-serve` 0.13.8 owns an accept loop with a permit acquired before `accept()`, a
connection ceiling, a per-message header-read timeout, a bounded shutdown drain, an RFC
9112 `Host` check, path and query length limits, and a single response exit point
(`route_with` → `route_inner` → `finalize`) that applies nosniff, CORS and HEAD
body-stripping to every branch. Twenty-five guarantees, each verified by removing it from
the source and confirming exactly one test fails (`verify-guarantees.sh`). It costs 5.04
µs/req on a static route against raw hyper's 4.59 — 0.45 µs of framework.
`mini-static` 0.32.1 owns a *second* accept loop, semaphore, header timeout and shutdown,
none of which are written down as guarantees and none of which are mutation-verified. It
has no `THREAT_MODEL.md`. It also owns a genuinely fast file-serving engine: at 0.31.0,
56,700 req/s at 98% CPU, 57,900 req/CPU-s, p50 0.87 ms — ahead of a same-run
single-worker nginx on throughput (52,500) and level on per-CPU efficiency (57,900),
without a content cache. **Those numbers have not been re-measured since 0.31.0**; 0.32.0
and 0.32.1 changed the resolution path.
`mini-unified` 0.2.0 bridges the two in 209 lines whose entire purpose is to discard
`mini-static`'s server half and keep its handler half.
The two crates independently derived three things:
- **Path decoding.** `mini-serve` splits then decodes (RFC 3986 §3.3); `mini-static`
decoded then split until 0.32.0. `%2F` was a literal to one and a separator to the
other. Composed, `/admin%2Fconfig` bypassed a 403 guard and served the file — proven,
then fixed in 0.32.0. Nothing structurally prevents the next divergence.
- **Method dispatch and 405**, with its own `Allow: GET, HEAD` (`server.rs:877`).
- **HEAD body-stripping** (`server.rs:1070`, `server.rs:1105`), where `mini-serve` strips
uniformly *after* the handler returns and has a verified guarantee that a HEAD response
reports the `Content-Length` its GET would (RFC 9110 §9.3.2).
Dependency weight: `mini-static` 33 crates, `mini-serve` 40 (7 of them the serde stack,
for JSON `mini-static` never emits), both together 43. Adding `mini-serve` to a
`mini-static` build costs 0.3s on a clean release build, 0.1s on debug, and 0.02s
incremental — measured, and the reason no feature flag appears in this plan.
## Where We Need To Be
One accept loop, one router, one path model, in one process.
- `mini-static` exposes its file-serving engine as a handler and has **no accept loop,
no method dispatch and no HEAD handling of its own**. It depends on `mini-serve`
unconditionally and re-exports it.
- `mini-serve` gains a `with_fallback` seam *inside* `route_inner`, receiving the request
and the segments the router already decoded. It gains no dependency on `mini-static`;
an API-only deployment pulls no file-serving code.
- `serve_dir(root, addr)` is the standalone path, implemented as a `mini-serve` app with
zero routes and a static fallback. The bare case is the layered case.
- `mini-unified` is deleted.
- Serving files, API routes, TLS (transport seam) and WebSockets (upgrade seam) compose
in one process without a bridge crate.
## Acceptance Gates
Every gate is a pass/fail with a number or a named test. The migration is not done until
all four pass. Gates 1 and 3 are captured as baselines *before* any behaviour moves,
because neither currently exists.
| G1 | `mini-static`'s guarantees | Enumerated in `THREAT_MODEL.md`, every one mutation-verified. This is the checklist the migration must preserve. |
| G2 | `mini-serve`'s guarantees | All 20 mutations still caught, **with the response-shape tests extended to treat a fallback-served response as another shape** (nosniff, CORS, 5xx sanitisation, panic reporting, HEAD length). |
| G3 | `mini-static` throughput | ≥ **52,500 req/s** and ≥ **57,900 req/CPU-s** in the `oha` c=50 single-worker harness — i.e. still at or ahead of nginx. Re-baselined on 0.32.1 first. |
| G4 | `mini-serve` throughput | No regression in `bench/bench.sh` CPU-µs/req from adding the fallback seam. |
Criterion benches (`handle_request`, `path_resolution`) are necessary but **not
sufficient** for G3: they measure only the half that survives the migration and would
report no regression while end-to-end throughput fell.
## Commits
### 1. [static] Write down what mini-static already guarantees
- Does: adds `THREAT_MODEL.md` and `verify-guarantees.sh` covering today's behaviour —
traversal containment, fd-based `starts_with` check, encoded-separator refusal, hidden
files, the 404 oracle collapse, symlink escape, connection ceiling, header timeout,
shutdown.
- Verifies: `./verify-guarantees.sh` reports every mutation caught; a citation test proves
every named test exists. **This is G1.**
- Touches: `THREAT_MODEL.md`, `verify-guarantees.sh`, `tests/threat_model_citations.rs`.
- Reverts cleanly: yes — documentation and a script, no source change.
- Note: expect vacuous tests. The encoded-separator work found four in one afternoon.
- **Tripwire (P4): if more than five vacuous tests turn up, stop and split them into
their own commit.** "Fix whatever the mutations find" is an unbounded second reason to
exist hiding inside a documentation commit.
### 2. [static] Build the throughput harness, then baseline 0.32.1
- Does: adds `bench/throughput.sh` and the nginx config it compares against, then runs it.
**The harness does not currently exist** — `benches/` holds criterion micro-benchmarks
only, and the 56,700-vs-nginx figures came from an ad-hoc run that cannot be reproduced
or re-pointed at a composed server. G3 is unmeasurable until this exists.
- Verifies: three runs reproduce within noise, and reproduce 0.31.0's published numbers
when checked out at that tag. **This is G3's baseline.**
- Touches: `bench/throughput.sh`, `bench/nginx.conf`, `bench/README.md`, this file.
- Reverts cleanly: yes.
- **Records the machine.** The 0.31.0 numbers were taken on the development machine; the
T480 is the perf-verification box. A comparison across machines is not a comparison.
The harness prints host, core count and nginx version into its output.
- Note: if 0.32.1 no longer clears 52,500 / 57,900, **stop** — that is a regression from
the security fix, and this plan does not proceed until it is resolved.
### 3. [code] Teach the gate to check the non-default feature build
- Does: when a crate declares features, `gate.sh` runs its lint, tests and doc build a
second time under `--no-default-features`.
- Verifies: reintroducing a `--no-default-features`-only compile error is caught — proven
by making the check fail before trusting it, per the rule this gate already carries.
- Touches: `.claude/skills/code/gate.sh`, `SKILL.md`, and the copy in `~/.claude`.
- Reverts cleanly: yes.
- **Why it precedes commit 4.** The gate lints `--all-features` only. The moment commit 4
adds a feature, it creates a build configuration nothing checks, which is how the
unlinted state rots. This is the same failure the `--all-targets` widening fixed for
tests.
### 4. [serve] Feature-gate serde behind a default-on `json` feature — **deferred**
- **Moved to the end of the plan.** Gating requires either rewriting ~55 call sites across
15 test files, or accepting a `--no-default-features` build that compiles but is not
tested — the latter being the exact hazard commit 3's gate check exists to prevent.
Neither cost is justified before the layering has proven itself, and deferring costs one
line in `mini-static`'s manifest (`default-features = false`) rather than a re-release.
- Its prerequisite landed anyway and was worth it alone: `body_bytes` (`597a6c1`) moved the
request-body ceiling off the JSON path, where it had been reachable only through
`json_body` — so an application reading bodies any other way got no limit at all.
- Does: puts `json()`, `json_body()`, the `query_params`/`path_params` deserializers and
the 5xx JSON error body behind `json`, default on. The 5xx body becomes a plain string
when the feature is off.
- Verifies: `verify-guarantees.sh` catches all 20 mutations under
`--no-default-features`; crate count drops 40 → 34; `gate.sh` green in both feature
states (commit 3); `mini-socket` and `mini-tls` still build against it.
- **Check first:** the `5xx-body-sanitized` guarantee test may assert a JSON body shape.
If it does, gating JSON changes what that mutation proves, and the test must be
rewritten to assert the *absence of the internal message* rather than the presence of
a JSON envelope.
- Touches: `Cargo.toml`, `src/body.rs`, `src/response.rs`, `src/extract.rs`,
`src/app.rs`, `src/lib.rs`. ~60 LOC.
- Reverts cleanly: yes.
- Rationale: not build time (measured at 0.3s), but dependency weight — 7 crates of JSON
machinery in a static file server is what the ecosystem premise exists to refuse.
### 5. [serve] Add the fallback seam — **done** (`f0f997a`, `49a8f03`)
- Does: `RouteBuilder::with_fallback(handler)`, invoked **inside `route_inner`** when no
route matches, receiving the request and the already-decoded path segments.
- Verifies: a fallback-served response carries nosniff and CORS, has its 5xx body
sanitised, survives a panicking fallback, and strips its HEAD body — asserted by
extending the existing guarantee tests, then confirmed by re-running their mutations.
**This is G2.** Plus G4: `bench.sh` shows no CPU-µs/req regression.
- Touches: `src/app.rs`, `src/router.rs` (expose segments), tests. ~120 LOC.
- Reverts cleanly: yes — additive; no existing caller changes.
**The fallback MUST pass through `apply_middlewares`.** Middleware is applied per route at
registration (`app.rs:1068`–`1096`); a fallback wired in anywhere else is *not wrapped*,
so an auth middleware guarding `/admin/` would not protect a file the fallback serves.
That is the identical failure to the encoded-slash bypass this whole plan exists to
prevent — reintroduced by the seam's plumbing rather than by a decoder. This gets its own
guarantee, its own test, and its own mutation id (`fallback-is-wrapped`): delete the
`apply_middlewares` call on the fallback and a middleware-blocks-fallback test must fail.
**Placement is the other half.** Wired *around* `route_with` instead of inside
`route_inner`, the fallback silently bypasses `finalize` — nosniff, CORS, HEAD stripping,
all of it. Both failures are invisible in a passing test suite; only the mutations catch
them.
### 6. [static] Expose the file-serving engine as a handler
- Does: adds a public handler entry point taking method, decoded segments and headers,
returning a response. Structurally typed — `http`/`hyper` types only, no `mini-serve`
types. The existing server stays and calls it.
- Verifies: the entire existing suite passes unchanged, routed through the new entry
point. No behaviour change.
- Touches: `src/server.rs`, `src/lib.rs`. ~80 LOC.
- Reverts cleanly: yes.
- **Blocked on the `Location` open question.** If redirects need the raw path, the
signature is not segments alone and this commit has a different shape. Resolve that
question before starting, not during.
### 7. [static] Adopt mini-serve; delete the second accept loop
- **Redesigned before starting.** The plan said to *replace* `run`/`run_on`/`run_all`/
`run_ephemeral` with `serve_dir`, making this the breaking commit. Measuring first
showed that costs ~100 call sites across 8 test files — 47 `run_ephemeral`, 53
`shutdown` — none of which the estimate mentioned. But the goal was never to change the
API; it was to delete the second accept loop. Keeping the existing entry points and
**reimplementing them on top of `mini-serve`** achieves that with no breaking change, no
test churn, and castra's frontends untouched. `serve_dir` becomes a convenience over the
same app rather than a replacement for anything.
- Does: takes an unconditional dependency on `mini-serve`, re-exports it, rebuilds
`run_on` as a `mini-serve` app with a static fallback, and deletes `accept_and_permit`,
`serve_connection`, the accept backoff, the semaphore and the drain handling.
- Verifies: G1 (static's own guarantees still caught — connection ceiling and shutdown now
satisfied by `mini-serve`'s verified versions, cited as inherited), G3 (throughput holds).
- Touches: `src/server.rs` (−~280 LOC), `Cargo.toml`, `THREAT_MODEL.md` (an
"Inherited from `mini-serve`" table, as `mini-socket`'s has).
- Reverts cleanly: yes. **No longer a breaking change** — the public surface is unchanged,
so castra's frontends are unaffected and the bump stays a patch.
- **The rest of the builder surface needs a decision, not a discovery.**
`with_max_connections` duplicates `mini-serve`'s ceiling; `with_request_logging` and
`with_response_header` duplicate middleware. Each either forwards to `mini-serve`,
or is deleted with the migration path documented. Enumerate them before starting —
finding them one at a time during the commit is how it blows its estimate.
- **The live-reload watcher has no owner once the server is gone.** `start_watching` and
the `Broadcaster` are spawned by the current `run_*` path. `serve_dir` must take over
that lifecycle, and a fallback registered by hand must have a documented way to do the
same, or live reload silently stops working in the layered case.
- **Do not publish until commit 10.** Commits 7 and 8 are only safe together, and the
gates have not run yet.
### 8. [static] Delete the duplicated method dispatch and HEAD handling — **dropped**
**Both halves were wrong, and an experiment said so rather than a reading.**
- **The 405 must stay here.** `mini-serve` answers `405` only for a path registered under
*another* method. This crate registers no routes at all — it is one fallback — so every
method reaches the fallback and there is nothing for `mini-serve` to refuse. Deleting the
check and running `tests/methods.rs` returns **200 for a `DELETE`**. That also settles the
grill's open question about which methods reach a fallback: all of them, and a fallback
owns its own method policy.
- **The HEAD handling must stay too.** `mini-serve` strips HEAD bodies in the connection
layer, which a direct `respond`/`handle_request` call never reaches. Neutralising this
crate's two HEAD branches fails `head_method_includes_content_length_but_empty_body`,
which calls the handler directly. Since `respond` is a public engine meant to drop into
any hyper stack, it has to be RFC 9110-correct on its own rather than relying on whoever
runs it. It is also the better implementation: this crate skips producing a body at all
for HEAD, where `mini-serve` discards one that was already built.
So this is not the duplication it looked like. The two live at different scopes —
engine-level correctness and connection-level enforcement — and both are load-bearing. The
feared `Content-Length: 0` conflict does not occur: this crate keeps the real length and
empties the body, and `mini-serve`'s strip is then a no-op.
### 9. [unified] Retire mini-unified — **done** (`e111337`), short of the yank
- Does: `mini-static` gained a public `into_fallback()`, which is what this crate existed
to provide; `mini-unified`'s README now says it is superseded and shows the migration,
and its description marks it deprecated on the registry.
- Verifies: `tests/composed.rs` in `mini-static` expresses the composed deployment without
it — API routes plus files, one router, one path model — and
`an_encoded_separator_cannot_bypass_a_guard_on_the_prefix` fails with a **200** when the
segment check is removed, so the original vulnerability is now a regression test in its
real setting rather than a scratchpad probe.
- **Not yanked.** A yank is an outward-facing registry action and is the maintainer's call,
not something to do in passing. Nothing depends on the crate: the only reference anywhere
is castra's `FORBIDDEN_PACKAGE_NAMES`, which asserts its core does *not* depend on it.
- The crate still builds and is still safe against current versions — the escape is closed
on the `mini-static` side regardless of the caller — it merely decodes the path twice for
no reason and is no longer the supported way to compose the two.
### 10. [static] Run every gate and publish
- Does: no source change. Runs G1–G4, records results in the table below, publishes.
- Verifies: all four gates green, recorded with numbers, not adjectives.
- Reverts cleanly: n/a — publishing is the one irreversible step in this plan.
- **Publish order is `mini-serve`, then `mini-static`.** `mini-static` depends on it, so
the reverse order cannot resolve. `mini-unified`'s yank goes last.
- **If a gate fails here**, the answer is not to lower the bar. Commits 7 and 8 revert
together and cleanly; the crate returns to its own accept loop while the cause is
found. That is the rollback, and it is why nothing publishes before this point.
## Open Questions
- **Does a CORS preflight for a fallback-served path get a 204 or a 404?** `mini-serve`
guarantees that *a preflight for an unregistered path 404s rather than being masked by a
204* (`cors_preflight_only_for_registered_routes`). A catch-all fallback makes "registered"
ambiguous — every path is now servable. Either the guarantee's meaning changes or the
fallback must not receive `OPTIONS`. **Decide before commit 5**; the guarantee's wording
has to change in the same commit that changes its meaning, or the threat model starts
lying.
- **Which methods reach the fallback?** If `with_fallback` catches every method, then
`mini-serve`'s 405 machinery never fires for static paths and commit 7 removes a
behaviour without replacing it. If the fallback is registered per-method, 405 works
naturally but the API is clumsier. **Decide before commit 4** — it determines whether
commit 7 is a deletion or a migration.
- **Does the fallback see segments before or after the router's own allocation?** The
performance argument in this plan assumes the segments are computed once and handed
over. If the seam ends up re-deriving them, the composed path does the work twice and G3
is at risk.
- ~~**`Location` headers are built from the raw request path.**~~ **Resolved.** A redirect
must echo the client's *own* encoding — `redirect_location_preserves_percent_encoding`
pins `/my%20docs` → `/my%20docs/` — and re-encoding from decoded segments is not a safe
round-trip: `%41` would come back as `A`, silently changing the URL. So the handler
takes both, with a strict division: **decoded segments decide which file is opened; the
raw path is echoed into `Location` and is never used to resolve anything.** The
signature becomes `respond(&Request<B>, segments: &[String])` — method, headers and raw
path all come from the request, and every type in it belongs to `http`/`hyper`, so the
handler carries no `mini-serve` types and drops into any hyper stack.
- **castra's three frontend servers are pinned at `mini-static = "0.7.1"`**, twenty-five
minor versions behind, and commit 6 breaks their entry point. They need an upgrade pass
regardless; it is out of scope here but it is a real downstream consequence.
- **`mini-static`'s 0.32.1 numbers are unmeasured end-to-end.** Commit 2 may find the
security fix cost more than the criterion benches showed, in which case this plan pauses
at commit 2.
- **No `--no-default-features` handler-only mode.** Measured DX benefit was 0.02s in the
dev loop, so the flag is not justified by build time. If someone wants the engine inside
axum or raw hyper, add it then — with a user asking, rather than on a hypothesis.
## Recorded Results
| G1 static guarantees | **17/17 caught** (commit 1) | | |
| G2 serve guarantees | 19/19 caught (pre-work) | **21/21 caught**, incl. `fallback-is-wrapped` and `fallback-gets-router-segments` | **pass** |
| G3 static throughput | 60,877 req/s, 60,950 req/CPU-s (commit 2) | **58,265 req/s, 58,405 req/CPU-s** | **pass**, by 0.9% |
| G4 serve CPU-µs/req | 2.43 µs `/health`, 2.70 µs `/hello/:name` (pre-seam 0.13.9, this machine) | **2.38 / 2.79 µs** — inside the harness's own noise | **pass** |
### G4, and the harness that could not run
`bench/bench.sh` had been broken since this repo became a workspace: a build inside
`crates/mini-serve` writes to the *workspace* target directory, and the script still
looked under the crate. The 5.04 µs/req figure quoted from it was therefore not
reproducible — the same defect as G3's unrecorded machine, in the other crate. Fixed in
`3de70ed`.
Measured before and after the seam on one machine (aarch64, 8 cores), mini-serve moved
−2.1% on `/health` and +3.3% on `/hello/:name`. **Both are inside the noise**: `actix`,
whose code did not change between the two runs, moved 11.8%. The seam adds no work to a
route that matches — the fallback is consulted only where a flat `404` would have been
returned — and the measurement is consistent with that, without being precise enough to
prove it alone.
### G3 after the migration: the layering costs 4.2%
| mini-static 0.32.10, own accept loop | 60,877 | 60,950 |
| mini-static 0.33.0, on `mini-serve` | 58,265 | 58,405 |
| bar (the recorded historical position) | 52,500 | 57,900 |
Stable across four runs — 58,264 / 58,144 / 57,936 / 58,405 — so the 4.2% is real, not
noise; this harness varies by well under 1%, unlike the Docker one. Taking the segment
vector out of the request extension rather than cloning it recovered about 0.5%; the rest
is structural: `mini-serve`'s `Host` check, its path and query length limits, `finalize`,
and erasing the body to a `BoxBody`.
**It passes, with 0.9% to spare, and that is a thin margin worth stating plainly.** What
the 4.2% buys: one mutation-verified accept loop instead of two, one path model instead of
two, ~300 lines deleted here, `mini-unified` made redundant, and three connection-level
guarantees this crate did not have — including the RFC 9112 `Host` check, which it had
never implemented. Recovering the rest means optimising `mini-serve`'s per-request path,
which is worth doing on its own terms rather than inside a migration.
### G3 baseline, recorded
`bench/throughput.sh` on Mac16,2, 8 cores, macOS, nginx 1.31.3, oha c=50 for 10s after a
3s warm-up, one worker each, a 484-byte file:
| mini-static 0.32.5 | 60,877 | 100% | 60,950 | 0.82 ms |
| nginx (1 worker, sendfile) | 74,566 | 100% | 74,824 | 0.66 ms |
**No regression from the security fix.** Measured on the same machine, in a worktree at
`a353e0b`, mini-static 0.31.1 serves 60,839 req/s against 0.32.5's 60,535–60,877 — the
same within noise. The encoded-slash fix and the `Cow` recovery cost nothing end to end.
**But the README's competitive claim does not hold on this machine.** It states mini-static
is ahead of single-worker nginx on throughput and level per CPU-second, from a run where
nginx measured 52,500 / 57,900. Here nginx measures **74,566 / 74,824** — 42% faster than
in that run, while mini-static is 7% faster than its recorded 56,700. The gap did not open
because this crate got slower; nginx scales better on this hardware. On this machine nginx
leads by 22%.
That is a documentation accuracy problem, not a regression, and it is exactly what an
unreproducible benchmark hides. **G3's bar therefore stays absolute** (≥ 52,500 req/s and
≥ 57,900 req/CPU-s) rather than "at or ahead of nginx", since the latter is machine
dependent and the former is the recorded historical position.
## Grill Verdict
Round: 1
Status: PASS
Findings resolved:
- **[5, G1] The fallback would not be wrapped by middleware.** Middleware is applied per
route at registration (`app.rs:1068`–`1096`). A fallback wired in anywhere else is
unwrapped, so an auth guard on `/admin/` would not protect a file the fallback serves —
the identical failure to the encoded-slash bypass this plan exists to prevent, arriving
through the seam's plumbing instead of a decoder. Now an explicit requirement with its
own guarantee, test and mutation id (`fallback-is-wrapped`).
- **[5, G1] The CORS preflight guarantee changes meaning.** `preflight-only-real-routes`
says a preflight for an *unregistered* path 404s; a catch-all fallback makes every path
registered. Promoted to Open Questions with a decision required before commit 5, because
the guarantee's wording must change in the same commit as its meaning.
- **[2, G1] The throughput harness does not exist.** `benches/` holds criterion
micro-benchmarks only; the 56,700-vs-nginx numbers came from an ad-hoc run. G3 was
unmeasurable as written. Commit 2 now *builds* the harness, and records host, cores and
nginx version — a cross-machine comparison is not a comparison.
- **[4, G1] The gate cannot see a `--no-default-features` build.** It lints
`--all-features` only, so commit 4 would create a build configuration nothing checks.
Split out as commit 3, ahead of the feature it exists to cover.
- **[4, G3] The 5xx sanitisation test may assert a JSON body shape**, in which case gating
JSON changes what that mutation proves. Now checked first, and rewritten to assert the
absence of the internal message rather than the presence of a JSON envelope.
- **[7, G1] The builder surface beyond `run_*` was undiscovered work.**
`with_max_connections`, `with_request_logging` and `with_response_header` all duplicate
`mini-serve` facilities. Enumerated as a decision to take before the commit starts.
- **[7, G1] The live-reload watcher has no owner** once `run_*` is gone. `serve_dir` takes
the lifecycle, and the hand-registered case needs a documented equivalent, or live
reload silently dies in the layered configuration.
- **[6, G3] The handler's signature depends on the unresolved `Location` question.**
Marked blocked rather than discovered mid-commit.
- **[1, G2] "Fix whatever the mutations find" was unbounded** inside a documentation
commit. Given a P4 tripwire at five.
- **[10, G1] Publish order was unstated** — `mini-serve` must precede `mini-static`, and
the rollback if a gate fails at the end is now written down rather than improvised.
- **[9, G1] Deleting `mini-unified` could strand castra**, which intends to use the
pattern. Now gated on that consumer having moved.
Findings accepted as tradeoffs:
- **Ten commits across three repositories.** Larger than any plan in this stack so far.
Inherent: the seam has to exist in `mini-serve` before `mini-static` can plug into it,
and the gates have to exist before either moves. Recorded so it is entered deliberately.
- **Commits 7 and 8 revert as a pair, not individually.** Deleting method dispatch (8)
while `mini-static` still ran its own server would leave it with none at all. P3 is
satisfied for the pair rather than each commit; stated rather than pretended away.
- **castra's three frontends break at commit 7.** They are pinned at `mini-static
= "0.7.1"`, twenty-five minor versions back, and need an upgrade pass regardless. Out
of scope here, but it is a real downstream cost of this plan and not a free one.