WEBSERVE W0 — the serving substrate: node-prefixed URLs, the serving registry, the WEB surface

Fixes BigscreenVR/spt-bs-releases#248

Delivers #248 and the LOCAL half of #249 for milestone #272 (v0.68.0). #249 and #272 stay open: #249's cross-node half is W1, #272 closes with the milestone.

## What lands

- **ADR-0056 node-prefixed URLs** — the loopback server answers `/<node>/…` for every facet; bare `/` redirects (302) to `/<node>/`; `f/`, `docs/`, `a/`, `m/`, `bin/` + `install` are reserved at the router (404 by facet name, never a registry lookup); `/<node>/` renders the index page with a `?json` twin; a known `/<other-node>/…` answers 502 until W1. The docs server's bare paths remain a byte-true compatibility alias.
- **ADR-0057 serving registry** — `$SPT_HOME/serve/registry` (spt-store format + read path, daemon single writer). Served-name rule clause by clause; **Amendment 2** (ruled from the W0 smoke): history records name → last (path, kind), so a freed name is re-taken only by the path it last named, and adapter aliases survive deactivate/activate and `adapter update`. Verbs `spt serve add|rm|list`. Manifest `[adapter].web_short_path` registers a core-owned `adapters/<adapter>/web/` served root at activation.
- **ADR-0060 WEB surface** — the `WEB` row joins the surface vocabulary (default-on within the subnet, rule-addressable, non-attributable); nothing dials it in W0.
- **W0 rider — unknown manifest keys are named** — manifest structs keep no `deny_unknown_fields` by design, so a misspelled optional key was visible only by absence. `Manifest::unknown_keys` walks the parsed TOML against the schemars schema; `adapter add` prints `manifest: unknown key [<table>].<key> (ignored)` per key and proceeds.
- **Gate driver** `.github/ci/ws272-w0.py` — IR-76 three-arm pre-flight, disk-floor instants with free space logged (IR-59 rider), exit files per leg, a worktree-level lane lock (same-lane pool serialization is the invariant), and an environment scrub (a battery is not an endpoint: the identity trio the daemon-stop guard reads is dropped from every leg and named once).
- Docs: `docs-site/src/serving/overview.md` (new, in SUMMARY and `llms.txt`), manifest reference (+ Unknown keys), `reference.md` regen, STORAGE.md, MANIFEST.md, ADR-0057 Amendment 2 text, CONTEXT.md served-name entry re-stated to Amendment 2 (gater-approved: a ruled amendment falsified the sentence, corrected by replacement), CHANGELOG.

Requirements activated: REQ-WEB-URL-NODE-PREFIX, REQ-WEB-SERVING-REGISTRY, REQ-WEB-ACCESS-SURFACE, REQ-MANIFEST-UNKNOWN-KEY-WARN (`traceable-reqs check` exit 0 on both hosts).

## Commits

- `2c5d3187` chore(treqs): mint the WEBSERVE W0 requirements
- `1f37840e` ci: W0 gate driver with the IR-76 three-arm pre-flight, a lane lock, and an env scrub
- `1d8f6f2e` feat(webserve): W0 serving substrate — node-prefixed URLs, serving registry, WEB surface

Product tree at `1d8f6f2e` is identical to `02adfbc1` (the difference is the driver file only) and differs from the Linux-tested `0b9554b3` by the driver file and one `llms.txt` line (no Rust, no docs-site page bodies).

## Evidence map

**Linux — kitsubito `~/spt-w1/.worktrees/ws272-w0`, SOLO battery at `0b9554b3`, exit files under `.spt/gate-head2/`** (chain log: `procs-before: 0`, lane lock held):

| leg | exit |
|---|---|
| treqs | 0 |
| pool claim | 0 |
| prebuild (workspace bins) | 0 |
| xtask check (docs drift, spacerun, internal-codes scan) | 0 |
| clippy (`--all-targets -D warnings`) | 0 |
| nextest (`--no-fail-fast`, spt-store / spt-runtime / spt-daemon / spt) | 0 — `Summary [1053.275s] 2776 tests run: 2776 passed (1 slow, 2 leaky), 0 skipped` |
| mdbook build | 0 |
| driver | 0 |
| Amendment 2 field smoke (`.spt/gate/w0-smoke-am2.py`, fresh home, own port) | 0 — 20 CLI ops, 20 HTTP probes, 0 failures (`.spt/gate-head2/smoke.raw`; exit file `.spt/gate-am2/smoke.exit`) |

At `02adfbc1` (`.spt/gate-head4/`): xtask check 0 (llms.txt link validation included), treqs 0. At `20427963` (`.spt/gate-head3/`, product tree identical to `1d8f6f2e` minus the llms.txt line): driver `--self-test` 0 (lane lock refuses a second driver naming the holder, frees on exit), treqs 0, `--probe-lock` 0. The pre-rider tree (`2b209369`, `.spt/gate-am2/`) was also fully green there: nextest 2771/2771, mdbook 0.

**Windows — hfenduleam `.worktrees/ws272-w0`, battery at `02adfbc1` (product tree == `1d8f6f2e`), exit files under `.spt/gate-head-d/`** (launched through a read-back-proven scrubbed environment, lane lock held):

| leg | exit |
|---|---|
| treqs | 0 |
| pool claim | 0 |
| prebuild (workspace bins) | 0 |
| xtask check | 0 |
| clippy (`--all-targets -D warnings`) | 0 |
| nextest (`--no-fail-fast`, spt-store / spt-runtime / spt-daemon / spt) | 100 — `Summary [969.371s] 2803 tests run: 2801 passed (1 slow, 4 leaky), 2 failed, 0 skipped` |
| mdbook build (by hand; the driver stops at a red leg) | 0 |

The two Windows reds, classified by the gater, neither implicating W0:

- `spt::resident_service_e2e::a_declared_service_rises_with_the_daemon_and_reaches_the_cli` (79.1s) — `resident_service_e2e.rs:664` teardown LEAKED (a second `svcmock.exe` outlived the sweep, settle 60s budget). FLAKE-LEDGER row L42, 4th sighting; hertz's daemon-leak cluster records it.
- `spt::io_events_undriven_kinds_e2e::relay_backlog_and_live_deliveries_stamp_once_even_after_hook_poll` (0.87s) — `io_events_undriven_kinds_e2e.rs:87` `explicit daemon start failed: ` with EMPTY stderr. Unclassified: the CLI died before any daemon-start exit arm printed (a child failure prints `DAEMON_START_FAIL`), the rig discards the exit code, and by kill-ledger it was nobody's reap. Instrument lane + ledger row dispatched to hertz. Passed in the earlier battery at the same sha.

**Environment-leak rig note (battery #1, kept at `.spt/gate-head/`):** the first Windows battery was launched from a live-perch shell and inherited `SPT_ENDPOINT_ID`; its four reds were two `DAEMON_STOP_REFUSED` refusals (the product's endpoint guard, as designed), one WMI job-escape rung, and the L42 leak, and the refused stops left six test daemons pinning `target/debug/spt.exe`. Nothing from it is cited; the driver's env scrub in `1f37840e` is the guard, and the launcher (`.spt/launch-battery.py`) reads the child environment back before reporting a launch.

**Rig-defect note (voided window, kept as proof):** kitsubito `.spt/gate-head/` + `.spt/gate-head-driver.log` hold an interleaved run — a stopped local ssh left its remote chain alive and a second driver ran beside the intended one (one `Starting 2776 tests`, two `Summary` lines). The gater voided that window in full, including its isolated reruns and smokes; nothing from it is cited above. The lane lock in `9b3caa92` is the guard.

## Gate notes

- Every commit carries `Co-authored by: todlando` (grepped on the raw body).
- CHANGELOG `[Unreleased]` entry present; visible text carries no internal codes (measured with the scan's token classes; the REQ tags are HTML comments, the file's existing convention).
- Field acceptance per `GATE-W0-272-CHECKLIST.md` is the gater's.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_015SQWGdNjq4sUbTzPQMLEQA
