# WEBSERVE (#272) W3 — drift-gate test riders (hertz)

Composed by doyle 2026-09-06 ~16:20Z from `WEBSERVE-272-JIT.md` §W3, while todlando is in W0
step 3/4. Test-only lane per the dispatch split: hertz authors the cells, todlando builds the
generator/facet they exercise. Everything below was MEASURED at main `eb38b71a` on 2026-09-06;
re-measure at your base sha before trusting a line number.

## What exists today (the surface your cells extend)

- `crates/xtask/src/main.rs` — `gen` writes ONE generated page, `docs-site/src/cli/reference.md`
  (`gen()` @273); `check` regenerates + byte-diffs it and exits 1 with a named message
  (@282-288). Then, in order: `scan_published_docs_for_internal_codes()` over a two-entry path
  list (@454-457: `cli/reference.md`, `manifest.schema.json`), `check_llms_links()` (@973, every
  `](target)` in `docs-site/llms.txt` must resolve via `llms_target_exists` @1000),
  `llms_full()` (@1014) concatenates every page in `SUMMARY.md` order.
- xtask already has 40 `#[test]` cells in `main.rs` (tempdir-rooted fixtures, e.g. @2345 for
  `llms_target_exists`) — match that shape, no new harness.
- `crates/spt-runtime/src/manifest.rs` @2452 — the manifest JSON-schema drift gate (checked-in
  `manifest.schema.json` must equal the struct-derived schema). A new manifest key trips it
  until the schema is regenerated and committed.
- `CHANGELOG.md` is NOT on the docs site and is NOT drift-gated in any form. It carries
  traceability tags as HTML comments (`<!-- [doc->REQ-IO-MSG-EDGES] -->` at line 17 today) and
  `releases#N` references in section bodies.
- MEASURED for #265: none of the 7 installed adapters on this box ships a docs directory
  (`$SPT_HOME/adapters/_github/*` = `manifest.toml`, the exe, `strings/`, `state/`). The facet
  therefore needs a manifest key; ruled `docs_dir` (todlando's build, JIT §W3 amended).

## Rider A — REQ-DOCS-CHANGELOG-PAGE gains a `unit` stage (yours)

Contract todlando builds to (hand him this signature verbatim; he implements, you test):

```rust
/// CHANGELOG.md -> docs-site/src/changelog.md. Pure: text in, text out.
fn changelog_page(changelog_md: &str) -> String
```

Your cells (in `crates/xtask/src/main.rs` `#[cfg(test)]`, tagged `// [unit->REQ-DOCS-CHANGELOG-PAGE]`):

1. **Tags stripped.** Input containing `<!-- [doc->REQ-X] -->` and `<!-- [impl->REQ-Y] -->`
   produces output where `forbidden_codes_in(&out)` (the existing scanner @~300) is EMPTY.
   This is the trap: the generated page MUST join the `scan_published_docs_for_internal_codes`
   path list (assert that list contains `docs-site/src/changelog.md` — a const or a
   function returning the list; if it is a literal array, ask todlando to lift it to a const so
   the cell can read it), and CHANGELOG.md's own tags would fail that scan on the first run.
2. **Bodies untouched.** Every non-comment line of the input appears byte-identical in the
   output, in order (the page is a copy with comments removed and a generated-banner header,
   the same "> **Generated** … Do not edit by hand." shape as `cli_reference` @144-148).
3. **Drift red/green (the mutation cell).** tempdir root with a `CHANGELOG.md` and a stale
   `docs-site/src/changelog.md`: the check arm reports drift naming `changelog.md`; after the
   gen arm writes it, the check arm passes. Refactor note for todlando: `gen(check: bool)` @273
   reads `repo_root()` and builds the binary via `spt_bin` — the changelog half must be a
   separate root-parameterised fn (`gen_changelog(root: &Path, check: bool) -> Result<(), String>`)
   so your cell needs no cargo build. The CLI-reference half stays as it is.
   **RULED 2026-09-06 16:20Z (hertz stop-and-refer, measured):** CHANGELOG.md at `eb38b71a`
   carries two VISIBLE forbidden tokens, `(releases#222)` at lines 262 and 273, besides the
   comment at 17. The generator strips HTML comments ONLY and never sanitizes bodies (A2
   stands); the W3 PRODUCER (todlando) rewrites those two body lines in the same commit, so
   the scan then gates every future CHANGELOG entry at the author's desk. Add cell **1b**: an
   input whose body carries a visible `releases#1` yields output where `forbidden_codes_in`
   is NON-empty — proof the page is gated, not laundered.
4. **Index membership.** `docs-site/src/SUMMARY.md` lists `changelog.md` AND `docs-site/llms.txt`
   links it (so `check_llms_links` holds it and `llms_full` carries it). Cell: read both real
   files from `repo_root()`, assert the entries exist — this is a repo-state cell, deliberately.

Mutation proof at your gate (driver, not a cell): append a line to `CHANGELOG.md`, run
`cargo run -p xtask -- check` → exit 1 with the changelog drift line in stderr; `-- gen`, then
`check` → exit 0. Read the exit FILES. Then revert the CHANGELOG line.

## Rider B — REQ-WEB-ADAPTER-DOCS-FACET, the drift-gate half (yours)

**Placement RULED 16:25Z: `[adapter].docs_dir`**, beside W0's `[adapter].web_short_path`
(ws272-w0 manifest.rs:230) — one table for the adapter's web-facet knobs. (A "top-level" pin
I sent at 16:20Z was inferred from a fixture's append shape, not measured; retracted. A second
error of mine, 16:22Z, claimed `HARNESS_COPY`'s last header was `[shell]` — a bad awk range
that ran past the raw string into the next consts. Measured 18:08Z: `HARNESS_COPY` has ONE
header, `[adapter]`, ending registry.rs:861; hertz proved it by parsing the composed fixture.
The cells' fixtures were correct as written; hertz adds a placement-pinning assertion.)

todlando adds `docs_dir` to the manifest (adapter-relative, must stay inside the adapter dir,
same refusal shape as `[strings]` pointers per `docs/MANIFEST.md` @342). Your cells:

1. **Schema drift.** The existing gate @manifest.rs:2452 already trips; your addition is the
   regenerated `manifest.schema.json` carrying `docs_dir` WITHOUT an internal code in its
   description (the scan @452 reads that file). Cell: parse the checked-in schema, assert the
   `docs_dir` property exists and `forbidden_codes_in(description)` is empty. Tag
   `// [unit->REQ-WEB-ADAPTER-DOCS-FACET]`.
2. **Escape refused at register.** `docs_dir = "../x"` and an absolute path fail manifest
   validation with the one-line error (mirror the `[strings]` pointer refusal: the escape
   test is `crates/spt-runtime/src/registry.rs` @190-196, its cell @~1394 — measured at
   `eb38b71a`). Missing `docs_dir` is valid (no adapter ships docs today).
3. **`--help` regen.** If the facet adds any CLI surface (todlando says; W3 plan is a URL
   facet, likely no verb), `cli/reference.md` regen rides the existing gate — nothing for you
   unless a verb appears.

The router/404-with-facet-name/int cells over `/<node>/a/<adapter>/…` are todlando's (they
need the W0 router); do not author them.

## Sequencing + lane shape

- Branch `test/ws272-w3-drift` under `.worktrees/ws272-w3-drift`, own pool, claim from inside.
  Author cells 1/2/4 of Rider A and cells 1/2 of Rider B NOW against the signatures above;
  they are RED until todlando's W3 branch exists. Do not open a PR red.
- When `build/ws272-w3` carries `changelog_page` + `docs_dir`, rebase onto it, run your cells,
  hand todlando the commit sha(s); they ride INSIDE the W3 PR (`Co-authored by: hertz` on your
  commits; pick-audit at land keeps provenance). One gate run, not two.
- treqs: W3 mints its REQ ids in todlando's lane (standing rule). Your tags reference ids
  that do not exist on main yet — that is fine on your branch; `traceable-reqs check` runs
  green only after the rebase. Do not mint them yourself (two minters, one toml = conflict).
- IR-12 residual rides here too (JIT §Register riders) — bring the entry text with you and
  say in the PR body which line discharges it.
