diff --git a/docs/superpowers/specs/2026-08-19-documentation-refresh-design.md b/docs/superpowers/specs/2026-08-19-documentation-refresh-design.md new file mode 100644 index 000000000..37400e33b --- /dev/null +++ b/docs/superpowers/specs/2026-08-19-documentation-refresh-design.md @@ -0,0 +1,1282 @@ +# Documentation Refresh (Full Surface) + +**Date:** 2026-08-19 +**Revised:** 2026-08-29 (round 15) +**Status:** Draft, pending review +**Scope:** Documentation and doc tooling. No runtime behavior changes. +**Baseline:** audited_target_tip `07dfc1c6d` (2026-08-28). The bulk +inventories were regenerated by a four-track delta audit at `a163367b3` +(the main→rc merge that landed #870); the ranges `a163367b3..985ff2298` +and `985ff2298..07dfc1c6d` (56 commits total) were each audited as +focused deltas and folded in below (notably: template secret references +restored and the placeholder consumer surface widened; a duplicate +commented `[trusted_client_ip]` template block introduced; +`proxy.allowed_domains` semantics broadened to signing + initial fetch + +redirects with rc's docs updated in the same commits; the +`validate_proxy_secret_strength` runtime validator REMOVED; `--scroll` +added to `ts audit ad-templates generate`; `browser_family` added to +device signals, auction telemetry, and the Tinybird datasource; the +`EdgeTerminatedAuthorization` digest for template-cache reuse; JS +`types.ts` renamed `suppressionConsumed` to `publisherRegistrationClosed` +and `proxy_sign` returns an outcome union). +Baseline contract - a merge-base check alone false-greens when the target +advances without a rebase, so the guard is exact-tip: after fetching, +`origin/rc/202608` must equal the recorded audited_target_tip AND the +implementation branch must contain that commit; the same exact-tip +assertion (never a merge-base comparison) repeats at the final rc-PR +HEAD. The containment work targets `main`, so an audited_main_tip is +recorded when the containment PR is cut and the same exact-tip assertion +runs before that PR merges. If any assertion fails, rebase, re-audit the +delta, and update this spec first. Rounds 1-8 of review history live in git; this +revision supersedes their inventories. + +## Context + +Trusted Server's documentation spans the VitePress site (`docs/`), root and +per-crate markdown, in-code documentation (rustdoc, clap help, JSDoc), and +configuration templates. The original audit found systemic drift; the +release branch has since fixed a meaningful subset itself and changed the +runtime model underneath the rest. Current state at the baseline: + +**Already fixed on rc (removed from this spec's worklist):** + +- The fabricated `GET /first-party/ad` / `POST /third-party/ad` endpoints + are gone from all four pages that carried them; `api-reference.md` now + documents `POST /auction` and the `/_ts/admin/ec`, `/_ts/admin/ec/{id}`, + and `/_ts/admin/eids` diagnostics with an auth-coverage contract. +- `format.yml` now runs `vitepress build` on PRs (dead internal links fail + CI). Note the interaction: with no `srcExclude`, CI now builds all 133 + internal `docs/superpowers/**` files as site pages. +- `cli.md` covers `active-version`, `healthcheck`, `rollback`, and + `config gc`; `configuration.md` documents the secret-store migration + (all 11 secret paths), `[trusted_client_ip]`, and the config-first + `[auction.providers.]` model; `getting-started.md` was rewritten + around the blob + secret-reference flow. +- Spin no longer hardcodes the example config: startup reads the blob from + Spin's `default` KV store and resolves secret references through Spin + variables. The old blocking follow-up is closed. + +**Still open (verified at the baseline):** + +1. **Publishing and policy hygiene.** No `srcExclude` in + `docs/.vitepress/config.mts` (133 internal spec/plan files build into + the public site); `docs/guide/index.md` is 0 bytes; the nav Guide link + targets `/guide/getting-started` and a Business Value nav item points at + `business-use-cases.md` (uncited quantitative claims; presents planned + headless-browser malvertising detection as shipped while `roadmap.md` + calls it planned); `docs/public/CNAME` is the literal + `your-custom-domain.com`; `fastly.toml` carries a real personal email + (line 4), the real service id (line 10), the orphaned + `test-prebid-eids.sh` comment (line 38), and inconsistently labeled key + fixtures; `docs/package.json` is ISC and not `private`; + `docs/guide/onboarding.md` publishes internal contacts and access + guidance. +2. **Fabricated or dead content that survives.** + `architecture.md:93-104` still shows the nonexistent `RequestWrapper` + trait (also in `.claude/agents/code-architect.md:16`); `ad-serving.md:11` + still documents Equativ (also `.claude/agents/issue-creator.md:85` and + `FAQ_POC.md`; it is gone from `integration-guide.md`); + `.with_asset(...)` remains in `creative-processing.md:808` and + `integration-guide.md:84,248`; `error-reference.md:614` still says + `npm run type-check`; `configuration.md:2301` still imports the + nonexistent `settings_data::get_settings`; the auction README's rotted + route table, `providers/` directory, and APS `mock` sections; + `onboarding.md`'s dead `SEQUENCE.md` links; `TESTING.md` is still the + auction curl runbook; `FAQ_POC.md` is still false on every axis. +3. **References behind the new runtime model.** `Settings` now has 17 root + fields (new `Option`; `request_signing` and + `creative_opportunities` are also `Option`), but `configuration.md` + still has no `[consent]` or `[debug]` reference sections, documents + `[tinybird]` only inside Quick Start, and its "Key Sections" table + lists 10 of 17 roots. Docs claim reserved-field protection for both + `request_ext` and `imp_ext` while `reject_reserved_fields` guards only + `request_ext`. `adserver_mock` is doubly stranded: rc deleted its old + config subsection without a replacement page. The template now + carries a DUPLICATE commented `[trusted_client_ip]` block (lines + 73-80 and 145-151, near-identical). The CHANGELOG carries 8 + breaking `[Unreleased]` entries with inconsistent `**Breaking**` + formatting and two dead `v1.2.0` compare links (no tag exists). +4. **Adapter truth gaps.** `/health` is not registered on Cloudflare; + `/_ts/admin/eids` is a real handler on all four adapters while + `/_ts/admin/ec{,/{id}}` are registered everywhere but functional only + on Fastly (KV-backed) and key rotation returns 501 off Fastly; + Cloudflare and Spin reject multi-provider auction plans at startup + (capability `concurrent_provider_fanout = false`; dormant configs are + accepted); startup-failure behavior differs (Spin: hardened 503 router + that keeps `/health` alive; Cloudflare/Fastly: 500; Cloudflare/Axum + degraded routers answer errors on every path); `Hooks::stores()` is + implemented only by Fastly, so on Cloudflare and Spin the request-time + config/KV registries are empty - the declared `TRUSTED_SERVER_KV` + binding is never opened, Spin's `v_current_x2dkid`/`v_active_x2dkids` + variables are unreachable, the Cloudflare `platform.rs:579-592` rustdoc + describing injected handles is false, and `cloudflare.toml` is dead + config referenced by nothing live. A bare `fastly compute serve` from + the checked-in `fastly.toml` cannot start the app: the + `trusted_server_config` store is empty and `ts_secrets` lacks the three + required keys, so every non-health path returns 500. +5. **Missing coverage.** No pages exist for Cloudflare, Spin, or Axum + deployment, EdgeZero, telemetry/Tinybird, tsjs, GPT slot handoff, + script guards, parity testing, or `adserver_mock`; seven of ten crates + have no README; the integration guide's snippets still do not compile + (`RuntimeServices` omissions, `fastly::http` import in core-neutral + code); `integrations-overview.md` covers 7 of 14 IDs. +6. **No enforcement beyond the new docs build.** No `cargo doc` in CI, + doctests never run (cross-compile only), no parity between code and the + hand-maintained inventories, `eslint-plugin-jsdoc` inert, + `openrtb-codegen` missing `[lints] workspace = true`, the PR template + still says `tracing`, and the slash-command files omit Spin/parity + gates. + +Appendices A-E carry the regenerated inventories with citations. + +## Decision + +Treat documentation as a product surface with a defined source of truth per +artifact, fix the verified-open findings in eight work packages, and add +enforcement, including executable parity checks bound to the reader-facing +markdown, so drift is caught by CI instead of by the next manual audit. +Every claim in the refreshed docs must be verifiable against code at the PR +HEAD's merge base with `rc/202608`; anything aspirational is labeled or +removed; adapter support claims come from an owned support matrix grounded +in current operational evidence. + +The source-of-truth map: + +| Artifact | Truth source | Consumers | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- | +| HTTP API reference | Adapter route tables and entry points (`adapter-*/src/{app,main,lib,platform}.rs`) + core handlers | Publishers, partners | +| Config reference | `Settings` (17 roots, `deny_unknown_fields`) + the typed per-integration configs + the provider profile schemas (`PROFILE_REGISTRATIONS`) + `secret_fields()` | Operators | +| CLI reference | The built `ts` binary's recursive `--help` tree (Linux + macOS) | Operators | +| Integration pages | The named inventories in Appendix C: deploy IDs (14), registry `builders()` (11), plan registrations (prebid, aps), profile registry (3), mediator (`adserver_mock`), JS modules (12) and bundles (13) | Publishers, integrators | +| Integration guide snippets | A compiling sample integration (`testlight` or a doc-tested fixture) | Integrators | +| Deployment guides | Adapter manifests + per-adapter startup paths (Appendix D) + support matrix | Operators | +| Architecture | `Cargo.toml` workspace members + `core/src/platform/` + `AuctionPlan` | Contributors | +| Test/CI docs | `.cargo/config.toml` aliases + `.github/workflows/*` | Contributors | + +## Source sets + +Truth-pass acceptance and parity checks operate on defined source sets: + +- **Active public set:** everything VitePress builds (`docs/**` minus the + WP1 `srcExclude` list). +- **Active repo set:** root markdown (`README.md`, `CONTRIBUTING.md`, + `TESTING.md`, `CHANGELOG.md`, `FAQ_POC.md` until actually retired, + `ProjectGovernance.md`, `AGENTS.md`, `CLAUDE.md`), crate READMEs, config + templates (`trusted-server.example.toml`, `fastly.toml`, `edgezero.toml`, + `.env.example`, `.env.dev`), and `.claude/commands/*.md`. +- **Active maintained internal set:** `docs/README.md`, `docs/internal/**` + (including the moved onboarding page), `docs/epics/**` (maintained + internal records), `docs/business-use-cases.md` while excluded-but- + tracked, `scripts/README.md`, `tinybird/README.md`, and + `tools/docs-parity/README.md` once created, and human-facing comment + regions DISCOVERED across all tracked operational files rather than a + narrow directory list: `.github/workflows/**`, `.github/actions/**`, + issue forms, every tracked `**/*.sh` usage header (crate-local scripts + included - `crates/trusted-server-openrtb/generate.sh:23` already + carries a false claim that `.cargo/config.toml` defaults to wasm32, + which WP2 repairs), `.cargo/config.toml`, and the comment surfaces of + the adapter manifests (`fastly.toml`, + `wrangler.toml`, `wrangler.ci.toml`, `spin.toml`, `axum.toml`, and + `cloudflare.toml` until retired), `.claude/skills/**`, + `.claude/agents/**`, and `.github/pull_request_template.md`. The + candidate universe is mechanical and complete: it is derived from the + all-tracked text/binary classification manifest (WP8a) - EVERY tracked + file classified as text is a candidate (a path whitelist cannot + recognize an extension it does not know; `.mjs` build scripts and the + human-facing `.proto` documentation are real examples outside any + whitelist), and every candidate must carry an explicit include or + typed-exclude disposition in the checked maintained-source manifest + (`{path, mode, selector}`; whole-file vs comment-region). Comment + regions are closed the same way files are: every comment-region file + class needs a supported comment extractor (fail closed when none + exists for a class), and EVERY extracted comment span must be + included or typed-excluded - so a new human-facing comment added + outside an existing selector fails the gate rather than passing + because its file is already classified. An unclassified candidate or + span fails the gate, so a new Dockerfile or + unfamiliar operational format cannot silently fall outside the + universe; the path rules below are the default include hints, not the + universe. The WP8b inventory gate asserts final set + equality, with negative fixtures for a new operational extension and + a comment outside an existing selector. +- **Historical set:** `docs/superpowers/**` and shipped `CHANGELOG.md` + release entries. Exempt from retired-term greps only; privacy/secret + scanning covers ALL tracked files (see WP2). + +## Goals + +- Every endpoint, config key, command, flag, crate name, and code path + named in active-set documentation exists at the baseline, with + per-adapter availability stated where behavior differs. +- Every shipped operator- or publisher-visible surface is documented: all + 14 deploy-validated integration IDs, all 17 config roots, the provider + profile model, the adapters (with evidence-based maturity labels), all + `ts` commands, telemetry, and tsjs. +- The adapter support model is truthful and mechanically canonical: a + checked adapter-support record renders the matrix and every repeated + status summary. +- The public site publishes only intended pages, containment reaches the + live site (Pages deploys only from `main`), and internal details are + scrubbed from the public repository regardless of build exclusion. +- Sensitive real-world values are removed or covered by the typed, + expiring allowlist (the `fastly.toml` `service_id` is its one PROPOSED + exception, pending the ops-owned migration; it becomes an approved, + time-bounded entry only once open question 1 records an owner and + review date, which gates scanner activation). +- CI catches regressions: docs build (already live on rc), rustdoc with + broken-intra-doc-link denial, doctests, and semantic parity bound to the + reader-facing markdown. + +## Non-goals + +- No runtime changes. Code defects found by the audits are follow-ups + (list below), not in-scope work; parity checks add tests/tools only. +- No new documentation toolchains beyond the `tools/docs-parity` dev + tool and the SHA-pinned external link-checker action. +- No rewrite of `business-use-cases.md` marketing copy: it is excluded + from the build via `srcExclude` and carries a source-level unverified + banner (WP1). Open question 4 is closed to this disposition - an + evidence-based rewrite is not an option inside this refresh, because + the delivery graph allocates no PR to remove the exclusion, restore + navigation, and smoke the page; republishing it is a separate future + effort with its own publishing PR and acceptance. `roadmap.md` gets a + factual status pass only. +- No release management. The 8 breaking `[Unreleased]` entries are a + maintainer decision; the deterministic no-release CHANGELOG edit is: + normalize the `**Breaking**` marker formatting, keep `[1.2.0]` with a + "(tag v1.2.0 was never published)" annotation, remove its dead link + reference, repoint `[Unreleased]` to `v1.1.0...HEAD`. If a release lands + before merge, rebase and re-audit. +- No accessibility audit gate (stock VitePress theme); WP5 ships diagram + prose equivalents, local search, and `lastUpdated`. +- Not chasing 100% rustdoc coverage. "Full surface" for in-code docs means + the WP7 worklist plus the known-false rustdoc repairs (e.g. the + Cloudflare `platform.rs` stores claim), not every item. + +## Delivery shape + +The owner's standing instruction is one rc PR carrying spec plus work; +the concrete shape below (which adds the forced `main` containment PR and +package checkpoints) still awaits explicit confirmation - open question 7 +blocks implementation until it is given, and its answer is recorded with +owner and date. The delivery graph is four PRs delivered inside this refresh - +(a)-(d) - plus two named follow-on `main` PRs it must set up: (c2), the +post-merge activation PR, and (e), the release handoff. Each is a real +PR with its own audited_main_tip, because each mutates default-branch +files: (a) the single rc PR (#1049) +carrying the spec plus all packages; (b) the `main` containment PR; +(c) a `main` automation PR, which lands the workflow in +DISPATCH-ONLY form (no `schedule:` trigger, no rc-targeted Dependabot +entries) because scheduled runs and Dependabot would otherwise inspect +pre-refresh rc content and a docs-parity cargo root that does not yet +exist on any target branch. Activation is a separate NAMED `main` PR (c2) - it mutates +default-branch files, so it is a PR under this design's own protection +model, not a checkpoint - that adds the `schedule:` trigger and the +rc-targeted Dependabot entries once #1049 has merged, with its own +audited_main_tip. Scheduled +workflows and Dependabot read their configuration from the DEFAULT +branch, so both live on `main`; version updates use +`target-branch: rc/202608`. Security updates always target the DEFAULT +branch, and the dependency graph analyzes manifests from it, so while +`tools/docs-parity` exists only on rc it receives version-update PRs +but NO Dependabot alerts or security-update PRs - an explicit, +time-bounded risk closing at the release merge (temporary dependency +submission for rc is the mitigation if that window proves long); the +config validation covers both kinds and says so rather than +overstating coverage (security updates always +target the default branch; the config validation covers both kinds and +the wording distinguishes them). Pre-merge acceptance is not circular +and not privileged: the workflow is split into two jobs. A dispatch +validation job runs with `contents: read` only, no secrets, and no +mutating steps; it checks out and executes the supplied SHA, which must +be a full 40-character SHA satisfying an exact machine predicate: the API +reports it as `head.sha` of pull request **#1049** in this same +repository, with `base.ref == "rc/202608"`, state open, and not a +draft - or it equals the current `origin/rc/202608` tip. No other +notion of "approved" is used (so the job needs no +`pull-requests: read` beyond the public metadata read it already has; +if a review-state predicate is ever added, that permission is added +with it). Arbitrary refs, stale SHAs, other PRs' heads, and +fork-repository SHAs are rejected, proven by negative workflow +fixtures, one per rejected class. A separate +schedule-only issue-management job holds the job-scoped `issues: write` +and never executes code from a supplied SHA - it checks out only the +branch tip it is configured for. Pre-merge, the validation job runs on the rc PR head. +(c2) is the owned post-merge activation PR to `main`, tracked as an +issue filed in WP8b with a named owner: after #1049 merges into rc, +record a `merged_rc_tip`, dispatch that exact SHA, then land the +activation edit (schedule trigger + rc-targeted Dependabot roots). +Because `main` does not yet contain `tools/docs-parity` or the WP8 +workflows, c2's own checks cannot come from `main`: its validation job +checks out the trusted merged rc tip for the TOOL and a second checkout +of the c2 head for the FILES UNDER TEST, statically validating the +actual changed workflow and Dependabot file, and c2's acceptance +additionally requires a successful real scheduled run after activation +(including the schedule-only issue job). Required-check activation on +`main` is deferred until the release merge puts the tooling there - +that deferral is recorded in the WP8b branch-protection acceptance item +rather than leaving c2 blocked on checks that cannot report. (e) The handoff PR to `main`, which GATES branch deletion rather than +being triggered by it - deleting rc first would leave the schedule and +Dependabot pointing at a dead branch, the exact state this design +forbids. Ordering, normal release: verify rc landed on `main` -> merge +(e) retargeting to `main` -> verify -> only then delete rc. Ordering, +abandonment: merge (e) disabling/removing the automation -> verify -> +only then delete rc. Normal path - the workflow checkout and Dependabot +`target-branch` switch to `main` (the checkout could fall back dynamically but Dependabot's +target is static, so a `main` edit is unavoidable), an XS retarget-only +edit. Abandonment path - rc is abandoned WITHOUT merging: retargeting is +invalid because `main` then holds neither the refreshed docs nor +`tools/docs-parity` and its cargo root, so the handoff instead DISABLES +and removes the automation (schedule trigger, rc-targeted Dependabot +entries, link workflow) rather than pointing it at content that does +not exist; transplanting the tooling to `main` is a separate, +explicitly sized effort, never a silent XS edit. The two paths are mutually exclusive diffs, so this refresh does not +open a single speculative draft: it produces a tracked issue plus two +REVIEWED patch templates/runbooks (retarget, and disable/remove), and +the concrete PR is opened from the matching runbook once the outcome is +known, with the same dual-checkout validation c2 uses applied to the +concrete (e) diff before handoff. Both paths have a named owner, a +sequencing row, and a verification item (the automation must never keep +pointing at a dead or content-less branch); (d) the CNAME resolution +PR to `main`, cut when open question 2 resolves (the containment PR +NEVER carries it - WP1's earlier allowance is superseded). Every +`main`-target PR records its own fresh audited_main_tip and runs the +exact-tip assertion before merge. The rc PR carries +one reviewable commit (or small series) per package, with package-level +review checkpoints (acceptance evidence recorded before the next package +lands), generated-output changes in their own commits, and no squash on +merge. One exception forced by mechanics: GitHub Pages deploys only on +pushes to `main`, so the containment subset also ships as a minimal +separate PR straight to `main`: exactly the `srcExclude` change (covering +`superpowers/**`, `internal/**`, `epics/**`, `guide/onboarding.md`, +`README.md`, `business-use-cases.md`), the onboarding move/scrub, the +filled Guide landing page, and the nav edits that removing pages forces +(Guide link to the landing page; Business Value item dropped). Removing +every link to an excluded source is a containment invariant. Neither the +CNAME resolution nor the marketing-page disposition blocks or rides in it. +CodeQL gets `rc/*` PR triggers (WP8b) and joins the final gate list. +Rollback treats exclusions and scrubbing as non-rollbackable security +invariants: recovery reverts only the causal non-security commit or +redeploys a known-good artifact that retains them. Open question 7 asks +the owner to confirm this shape. + +## Work packages + +### WP1: Publishing and policy hygiene + +- `srcExclude` + onboarding move/scrub + Guide landing page + nav edits + (contents fixed in Delivery shape); the containment PR to `main` ships + first and its post-deploy smoke asserts: excluded URLs 404; site root, + the Guide landing page, and one reference page return 200 with expected + content. +- `business-use-cases.md` gains its source-level unverified banner + (asserted by WP1 acceptance). +- Resolve `docs/public/CNAME` (open question 2; both branches specified: + delete and re-smoke project URLs, or custom domain with `base: '/'`, + Pages/DNS/TLS setup, canonical+asset smokes, and a + project-owned-public-domain allowlist classification). Because only + `main` deploys, an rc-only edit never reaches the live site: the + resolved disposition ships as its own `main`-target PR (delivery graph + item (d); the containment PR never carries it). The custom-domain + branch also inventories hard-coded Pages URLs (e.g. `README.md:11`) + rather than testing only canonical and asset responses. Question 2 + plus its live smoke is a completion gate for this refresh. +- `fastly.toml`: empty `authors` list; label the key fixtures + consistently as local test fixtures; comment the four KV stores; remove + the `test-prebid-eids.sh` comment. `service_id` stays under its + allowlist entry pending the ops migration (open question 1). +- `docs/package.json`: `"private": true`, license Apache-2.0. +- `CLAUDE.md` policy amendment lands HERE (not WP6): the sensitive-data + policy gains the owner-approved exception taxonomy (vendor URL, + hash-pinned fake-credential fixture, historical example, service ID, + project-owned public domain - each with owner, rationale, expiry), so + the scanner and every later checkpoint rely on a policy that is already + canonical rather than knowingly violating the written one until WP6. +- `.github/pull_request_template.md`: `tracing` → `log`; test-plan gates + become a link to the canonical gate region (link-only mode). +- `.claude/commands/*.md` converted to link-only gate references; + `AGENTS.md` gets a generated gate region (it is the fallback for agents + that cannot read `CLAUDE.md`, so it carries the list). + +Acceptance: `vitepress build` output contains none of the excluded pages; +the `main` containment PR is merged and its smoke passes; the CNAME +disposition (open question 2) is selected and PR (d) is merged with its +branch-specific live smoke (project-URL re-smoke, or for the custom +domain: DNS/TLS evidence plus canonical, asset, and hard-coded-URL +checks) and its own audited_main_tip assertion; the banner is +present; no internal contacts or access instructions anywhere in the repo; +every command file links to (not copies) the canonical gates. + +### WP2: Truth pass over existing content + +Disposition-based: every document in all three active sets (public, +repo, and maintained internal - root markdown, config templates, +`edgezero.toml`, env files, and command files included) gets +verified / rewrite / retire / created recorded in a +checked inventory under `docs/internal/audits/`, stamped with the audited +merge-base SHA, with source anchors; non-page surfaces get region-level +dispositions. The inventory is an audit record; the WP8 gates are the +continuing control. Executable fences are governed by the WP8a snippet +manifest (all languages, graded modes, expiring waivers). + +Content items (all verified open at the baseline): + +- Remove `RequestWrapper` from `architecture.md` (replace with the real + platform traits) and from `.claude/agents/code-architect.md`; remove + Equativ from `ad-serving.md` (rewrite the page around the real flow), + `.claude/agents/issue-creator.md`, and via the `FAQ_POC.md` handling + below; remove `.with_asset(...)` (closes #277); fix `npm run +type-check` and the `settings_data::get_settings` example (the exported + loader path); retired-token cleanup for maintained agent files happens + here so WP2's own checkpoint grep can pass. +- Reserved-field truth fix: docs claim `request_ext` and `imp_ext` + protection; code guards only `request_ext`. Fix the docs to match code + and file the follow-up asking whether `imp_ext` should be guarded. +- Auction README repairs (route table by symbol name, real provider + layout: `AuctionPlan`, `PROFILE_REGISTRATIONS`, `GenericOpenRtbProvider`, + mediator; remove the removed-`mock` sections). +- `FAQ_POC.md` is a three-way decision (open question 3): retire, + archive under the historical tree, or factual rewrite - archive is + the deadline fallback; a rewrite carries its own acceptance (every + answer verified against code, page kept in the active public set), + while retire and archive leave no active-set page; + replace `gam.md`/`kargo.md` with tombstone content (routes preserved + unconditionally, `tombstone` orphan-allowlist kind, old-route smokes). +- `TESTING.md` rewritten as the test-matrix index; the auction runbook + verified-then-rewritten into `docs/guide/auction-testing.md`. +- CHANGELOG: the deterministic no-release edit (Non-goals) plus the + missing operator-visible entries check. +- Environment files: `.env.example`/`.env.dev` retired-key cleanup and the + two-surface model (runtime variables vs `ts config` overlay). +- Fictional-data pass with the typed allowlist (vendor URL / hash-pinned + fixture / historical / service ID / project-owned public domain), owner + - rationale + expiry per entry; scanning covers ALL tracked files + (negative fixtures are synthesized at test time, never checked in); + remediation of non-document fixtures (e.g. the scraped + `html_processor.test.html`) is owned here with regression tests re-run; + rotation/history-rewrite decisions escalate per finding. +- Human-facing workflow/script comment repairs: the Spin release-build + comment (claims env overrides fix embedded settings; startup now reads + the KV store), the `test-cli` comment, and + `crates/trusted-server-openrtb/generate.sh:23` (both claim a workspace + default wasm32 target that `.cargo/config.toml` explicitly does not + set). +- Roadmap status pass (shipped/active/deferred; also reconcile the + malvertising-detection claim with `business-use-cases.md`). +- Verified dispositions for the docs rc updated inside the audited + deltas (`allowed_domains` semantics in `configuration.md` and + `first-party-proxy.md`, the `/first-party/sign` 403 + `href`/`base` + contract in `api-reference.md`, the softened `proxy-signing.md` + recommendation, the cli.md `--staging` known-limitation callout): + confirm each against code rather than re-writing. + +Acceptance: checked-in inventory complete over all three active sets; +the retired-token scan is case-insensitive and whitespace-tolerant +(`rg -i` for prose identifiers; pattern classes for spacing variants such +as `mock\s*=\s*true`) for `RequestWrapper`, `Equativ`, `with_asset`, +`type-check`, `settings_data::get_settings`, `SEQUENCE.md`, +`synthetic_id` (outside shipped changelog entries), +`TRUSTED_SERVER__SYNTHETIC__` (the retired env-overlay root in +`.env.example`/`.env.dev`, with its own synthesized regression fixture), +and the APS `mock` key, returning nothing; the all-tracked privacy scan is clean modulo the +allowlist; checkpoint scope = surfaces this package touches, full-set +greps re-run at final HEAD. + +### WP3: Configuration reference completion + +Parity with the 17-field `Settings`, the typed integration configs, the +provider profile schemas, and the secret model. + +- `configuration.md`: add the missing `[consent]` and `[debug]` reference + sections; promote `[tinybird]` from Quick Start prose to a reference + section; complete the "Key Sections" table to all 17 roots; extend + Integration Configurations from 5 to all 14 IDs (audit the existing + five against the field inventory); generate the + `[auction.providers..profile_config]` reference from the three + typed profile schemas (each profile's fields, defaults, timeout + defaults, byte/depth/key limits, endpoint canonicalization, reserved + fields). +- `trusted-server.example.toml`: the template already carries commented + blocks for every root (`[consent]` L151, `[rewrite]` L141, + `[tester_cookie]` L147, `[image_optimizer]` L209, + `[[proxy.asset_routes]]` L183, osano L507 at the baseline), so the work + is an exact present/missing/repair inventory, not block creation: + validate every block against the field inventory, and REMOVE the stale + recommendations - the template still suggests the four legacy store + selectors (`proxy.asset_routes.auth.secret_store`, + `tinybird.secret_store`, + `integrations.datadome.server_side_key_secret_store`, + `integrations.datadome.protection_test_bypass.credential_secret_store`) + that the code accepts-and-discards and the migration guide tells + operators to delete, and remove the duplicate commented + `[trusted_client_ip]` block (the delta introduced a second, + near-identical copy). Preserve the template's contract: placeholder + strings stay in the rejection constants' exact forms and secret + references carry key names, never values. The literal-string consumer + surface is wider than three call sites since `985ff2298`: the config.rs + template tests, the CLI ad_templates substitutions, `config init`, the + audit `generate`/`validate` commands, and + `scripts/template-cache-local-test.sh` all splice on exact strings - + the WP8a harness enumerates these consumers and CI fails when a + placeholder change strands one. +- Directional field dispositions as independent axes, not a flat + "one of" (real fields overlap: `S3SigV4AuthConfig.secret_store` is + simultaneously deprecated, skip-serializing, and normalized away) - + lifecycle (canonical / deprecated / rejected), key identity + (canonical / alias, with `alias_of` - `s3_sig_v4` and `pub_id` are + aliases, while `S3SigV4AuthConfig.secret_store` is a deprecated + accepted field, not an alias), serialization (serialized / skipped), + runtime (active / normalized-away / deserialization-only), and secret + handling (store-resolved / deliberately-inline / none); removed + validators get a `rejected`/removed disposition too - the delta + deleted `validate_proxy_secret_strength` (no 32-byte runtime + enforcement remains; `proxy-signing.md` was already softened to a + recommendation on rc, which WP2 verifies rather than rewrites); the + generated reference renders every applicable axis, so + `tinybird.access_token_secret` reads as deprecated + skipped + + normalized-away simultaneously. `tinybird.access_token_secret` is + accepted-but-normalized-away (deserialized, then set to `None` and + never serialized) - not a deliberately inline secret; only + `trusted_client_ip.shared_secret` currently holds that classification. + Deprecated, ignored, and deserialization-only paths are documented as + such and never promoted into the template as normal fields. +- Secret-model documentation: classify every `Redacted` path with its + directional disposition - store-resolved (the 11 `secret_fields()` + entries), deliberately inline (`trusted_client_ip.shared_secret`, with + exposure guidance), or accepted-but-discarded + (`tinybird.access_token_secret`); the + migration section already on rc gets a verified disposition. CLI/config + pages warn that `ts config diff`/`--dry-run`/push output can print + inline-secret values. +- The extractor-based field inventory (WP8a) carries semantics: resolved + defaults via the literal-AST + companion-manifest + compiled-probe + chain; requiredness; grammars; ranges from `#[validate]`; every custom + validator in Appendix B's inventory gets a companion entry with + positive/negative probes, fail-closed on unclassified validator + functions; `serde(skip)` fields never become documented paths; + canonical keys vs deprecated aliases (`pub_id`, `s3_sig_v4`). + +Acceptance: every canonical-runtime-active field path appears in +template and reference; deprecated/ignored/deserialization-only paths +appear only with their directional disposition stated; every +deploy-validated ID has a config subsection matching its struct; the +WP8a harness passes; the parity checklist (Appendix B) is in the PR +description. + +### WP4: API reference completion + +rc already rebuilt much of the reference; this package brings it to the +contract standard and binds it to generated regions. + +- Route/availability tables become generated regions from the checked + route inventory (Appendix A), which records per-adapter availability + (Cloudflare lacks `/health`; `/_ts/admin/eids` real on all four; + `/_ts/admin/ec{,/{id}}` registered everywhere, functional only on + Fastly; key rotation 501 off Fastly; EC partner API, tester cookies, + and JA4 debug Fastly-only), method shapes (page-bids GET plus + denied-OPTIONS; sign/proxy-rebuild GET+POST; identify GET+OPTIONS), + the seven-method publisher fallback, and route families (literal / + template / config-derived / conditional with config source or + predicate - Prebid `script_patterns`, prefix overrides, APS renderer + route only in `trusted_server` rendering mode). +- Per-endpoint contract checklist for manually owned prose (auth, + schemas, response codes, cache/CORS, config gates, rate limits, + examples), with explicit ownership markers; `/first-party/sign` (mints) + vs `/proxy`/`/click`/`/proxy-rebuild` (validate) stay distinguished. +- Startup-failure behavior documented per adapter (Spin hardened 503 with + live `/health`; Cloudflare/Fastly 500; degraded-router differences). +- `trusted_client_ip` documented as middleware (sanitization on all + adapters, IP resolution only on Fastly), not a route. +- Route parity is mechanically closed over all four adapters, not just + Cloudflare: Fastly, Axum, and Spin expose named route collections that + the per-adapter tests snapshot directly; Cloudflare's inline + `build_router` chain is parsed by `docs-parity` with a fail-closed + grammar that expands the known constants and loops (path arrays, + `publisher_fallback_methods()`) - an unrecognized construct fails the + check rather than undercounting. Each adapter's snapshot covers the + full route set, methods, predicates, unsupported/guarded semantics, + and the startup-error router behavior, asserted as set equality + against the checked inventory - a new route on any adapter fails CI + until the inventory and generated regions update. + +Acceptance: generated regions match the checked inventory per adapter; +contract checklist satisfied; manual-ownership markers present. + +### WP5: New coverage pages and navigation + +- Deployment guides grounded in the audited startup paths, each with a + first-success smoke that provisions BOTH halves (config store and + secret store) from clean state: + - `fastly.md` additions + quick start: init/validate → + `ts config push --adapter fastly --local` → seed the three required + `ts_secrets` keys (exact copyable edit of + `[local_server.secret_stores.ts_secrets]` entries) → + `fastly compute serve` → `/health` → publisher request against a + stub origin → restore the mutated `fastly.toml`. All guide commands + are exact and copyable; "seed the keys" prose is not acceptance. + - `cloudflare.md`: an executable bridge, not a concept. Decided + retrieval path: after `ts config push --adapter cloudflare --local`, + read the envelope back from local Wrangler KV with an explicit + binding selector (the guide first defines the binding strategy: the + config store gets its own provisioned namespace mapped via + `EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME`, or reuses + the existing `TRUSTED_SERVER_KV` namespace with that mapping - one + of the two is chosen and provisioned before the first push, so the + clean-state journey never pushes into an unmapped namespace): + `wrangler kv key get trusted_server_config --binding +--local` (or `--namespace-id`), then double-encode it into + `{"app_config": ""}` with `jq`, and write it into a + gitignored generated manifest (the `wrangler.ci.toml` + + single-placeholder substitution pattern the integration harness + uses) or `.dev.vars` for `wrangler dev`; remote deploys set the var + via the dashboard/`wrangler` and provision secrets with + `wrangler secret put ` (a production flow - local secrets come + from generated `[vars]`/`.dev.vars`, never `secret put`). `--local` + variants are the documented default to prevent accidental remote + writes; cleanup removes the generated files. The guide carries the + exact copyable commands, warns that a green push does not configure + the Worker, and notes no `/health`, the single-provider restriction, + and the unwired request-time `TRUSTED_SERVER_KV`. The smoke + terminates with a publisher request asserting EXPECTED rewritten + content, not merely a response - the degraded error router also + answers, so status-only checks false-green. The preferred end state + is the CLI envelope-export follow-up, which retires the KV + read-back. + - `spin.md` (now writable - the runtime fix landed): `ts config push +--adapter spin --local` with the required + `EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME=default` + mapping (push writes SQLite under `.spin/`; runtime reads store label + `default` - the mismatch footgun is documented), Spin variable names + generated from the operator's key names via the encoder (empty + defaults fail closed; exact copyable `SPIN_VARIABLE_*` exports or + provider config), `spin up`, a non-health request, cleanup. + Maturity label from current evidence: experimental - no + integration-test environment, single-provider only, request-time + config/KV stores unwired. + - `axum-dev.md`: local development only; the env-var config/secret + bridge with exact variable names; read-only admin EIDs available, + key rotation and EC KV lookups not. +- Support matrix from a checked adapter-support record (generated regions + everywhere adapter status is stated), including provider fan-out + capability and startup-failure behavior columns. +- `edgezero.md` (manifest, stores, blob flow, lifecycle commands), + `telemetry.md` (+ `tinybird/README.md`; emission Fastly-only; covers + the new `browser_family` column through device signals, auction rows, + and the Tinybird datasource), + `tsjs.md` (module system including the third loading mode: + `gpt_diagnostics` standalone tag; 12 modules / 13 bundles), + `integrations/adserver_mock.md` (as the mediator, with + `auction.mediator` context - its old config subsection was deleted on + rc), GPT slot handoff in `gpt.md`, script guards in the integration + guide (closes #341), `testlight` reference section, compiling snippet + source for the integration guide. +- `integrations-overview.md` extended to 14 IDs + the creative row from + the capability record. +- Navigation restructure (Operator/Deployment/Reference groups), local + search, `lastUpdated`, rolling-main banner with build-SHA provenance + (`GITHUB_SHA` injected; smoke asserts it), diagram prose equivalents + with a checked inventory, journey walks in acceptance. +- CLI reference from the two-platform help union; description text passes + the internal-term gate with the expiring override table. + +- Every smoke shares one strong oracle - exact expected status, a + stub-origin sentinel present in the response, and an expected Trusted + Server rewrite or header proving the app (not a degraded router) + served it - plus INDEPENDENT negative cases per adapter, each with a + diagnostic matcher: (i) missing config blob fails with the expected + startup/config diagnostic; (ii) each required secret key, missing or + unresolved, fails for its expected reason; and a failure caused by an + unrelated launcher, origin, or port error satisfies neither case. One + negative run cannot stand in for both halves - both the config + handoff and the secret handoff must be proven live. This matters + concretely: Fastly's `/health` short-circuits before app construction + and succeeds while startup is broken, and Spin's degraded router + answers every publisher path with a valid 503, so status-or-response + checks false-green on both. Axum's smoke is the same full clean-state + sequence (export config + secret vars, launch, assert), not a list of + configuration facts. +- The four first-success journeys become recurring smoke scripts + (`scripts/smoke-{axum,fastly,cloudflare,spin}.sh`), each starting from + the documented CLI commands (not the harness's internal shortcuts - + today's Cloudflare integration coverage bypasses the push path), wired + into the integration-test workflow where runnable (Axum, Fastly, + Cloudflare). If Spin cannot run in CI, its manual evidence carries a + named owner, tested SHA and tool versions, and an expiry date - one PR + run is not continuing enforcement. + +Acceptance: every ID documented and nav-reachable; every adapter guide +consistent with the support record; snippets compile; `vitepress build` +green; search/banner/diagram/journey assertions recorded; the smoke +scripts pass in CI (Spin per its evidence contract); each adapter +smoke's exact commands and cleanup recorded in the PR description. + +### WP6: Root markdown and crate READMEs + +- Audit every existing root/crate/skill/agent document (deep audit here; + WP2 already removed falsehoods): `README.md` quick starts must satisfy + the first-success contracts; `CONTRIBUTING.md` refresh (link-only gate + reference); `CLAUDE.md` corrections (no workspace default target; the + integration-system section predates the plan model; `# Examples` + standard reconciled to the earn-their-keep rule; vendor-endpoint + exception sentence); integration-tests README fixes; governance doc + intent statements. +- New READMEs for the seven crates lacking one, plus `scripts/README.md`; + core README rewritten as a real overview; `readme =` keys in Cargo.toml. + +Acceptance: every `cargo metadata` package has a README; dispositions +recorded; quick-start journeys proven. + +### WP7: In-code documentation + +Worklist (acceptance scope): core `lib.rs` module index (13 bullets for +38 `pub mod`s today); `platform/` docs (4 of 10 files documented on rc; +test-only +excluded); crate-level headers for fastly/cloudflare/js/cli; module docs +for the undocumented core files (settings, settings_data, http_util, +proxy, auth, tsjs, openrtb, price_bucket, rsc_flight, host_rewrite, +storage, html_processor, registry, prebid, nextjs/, datadome/); +`constants.rs` items; CLI module docs; the TypeScript files (headers + +complete `core/types.ts`), `build-prebid-external.mjs` header. Plus the +known-false rustdoc repairs: the Cloudflare `platform.rs:579-592` stores +claim (and its Spin sibling) rewritten to match the unwired reality, +citing the follow-up. + +Rustdoc command matrix (self-contained; run with +`RUSTDOCFLAGS="-D warnings"`; the core/js documentation and native +doctest jobs need pinned Node because the js crate's build script runs +npm): + +- `cargo doc --no-deps --all-features -p trusted-server-core -p trusted-server-js -p trusted-server-openrtb --target wasm32-wasip1` +- `cargo doc --no-deps -p trusted-server-adapter-fastly --target wasm32-wasip1` +- `cargo doc --no-deps -p trusted-server-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare` +- `cargo doc --no-deps -p trusted-server-adapter-spin --target wasm32-wasip1 --features spin` +- `cargo doc --no-deps --all-features -p trusted-server-adapter-axum` +- `cargo doc --no-deps --all-features -p trusted-server-cli -p trusted-server-openrtb-codegen --target x86_64-unknown-linux-gnu` (CI; locally substitute the host triple) +- The adapter invocations deliberately pin explicit features instead of + `--all-features` because their features are target-gated (the + cloudflare feature carries a non-wasm32 `compile_error!`); everywhere + else `--all-features` applies, matching the repo's documented rustdoc + rule and covering core's test-utils feature. +- Native doctests: `cargo test --doc -p trusted-server-core` (host). + +The JSDoc contract is explicit and config-relative (ESLint runs from +`crates/trusted-server-js/lib`, so globs are `src/core/render.ts`, +`src/core/types.ts`, `src/core/registry.ts`, `src/shared/globals.ts`, +`src/integrations/creative/**`): `jsdoc/require-file-overview` enforces +the file-header block (`require-jsdoc` checks declarations, not +headers), and `jsdoc/require-jsdoc` covers every exported declaration +form - functions, classes, interfaces, type aliases, exported +variables/consts, default exports, and re-exports - plus +`jsdoc/check-alignment` and `jsdoc/check-types`. Each declaration form +and the file-overview rule get separate synthesized negative fixtures. + +Acceptance: worklist complete; matrix builds warning-free with +`RUSTDOCFLAGS="-D warnings"`; the WP8 jsdoc lint (mandatory, scoped as +above) green. + +### WP8: Enforcement (WP8a scaffolding early, WP8b activation last) + +WP8a (lands right after WP1): + +- `tools/docs-parity` (outside the workspace, own committed lockfile, + Dependabot cargo entry, nested-lockfile cache key, own README, host + fmt/clippy/test): the Serde-aware AST extractor (field and + container/variant attributes including `rename_all`/`tag`/`content`/ + `untagged`, fail-closed on unknown shape-changing attributes, companion + manifest for custom deserializers, validators, and nonliteral + defaults), generated-region generator (deterministic ordering, no-write + check mode, atomic updates), the Cloudflare builder parser (fail-closed + grammar above), CLI help goldens (both platforms, merged + platform-annotated union, description override table with owner/ + rationale/expiry/source-text staleness check), snippet-manifest tooling + (all languages; graded modes incl. `expected_compile_failure` and + `expected_validation_failure`, each carrying a REQUIRED stable + diagnostic matcher - error class/path or message pattern - and + expected phase, so a typo, missing dependency, wrong working + directory, or unrelated parser error cannot satisfy the gate, with a + fixture proving that a correct nonzero exit with the WRONG diagnostic + fails, and a negative example failing CI when it unexpectedly becomes + valid; `illustrative_fragment` entries carry owner, rationale, and + expiry like manual waivers, not a permanent escape hatch; expiring + waivers), the domain/credential scanner + typed allowlist (with a + defined input contract: text-vs-binary classification comes from path + plus a checked classification manifest, never content sniffing alone - + a tracked file the manifest does not classify FAILS the scan (so a new + binary cannot appear unreviewed, and text renamed under an unknown + extension cannot dodge content inspection); an expected-text file that + is oversized or invalid UTF-8 FAILS rather than being skipped + (thresholds must not become evasion mechanisms); classified binaries + get an ASCII/UTF-8 strings scan, or a reviewed hash-pinned manifest + entry where strings scanning is meaningless; generated lockfiles are + not blanket-excluded - their structured URL/source/registry fields are + scanned while checksum and integrity strings are recognized + structurally; known media assets get metadata-string inspection; + base64 test fixtures distinguish via the hash-pinned fixture allowlist + class - results must be deterministic across platforms. Every detector + and encoding class (domain, email, credential shape, service ID, + encoded token, binary strings) gets both a synthesized positive + fixture and an allowlisted fixture proving the allowlist path. The + scanner also carries a reviewed identifier denylist class - known + organization names, personal handles, chat channels, project IDs, and + access-instruction phrases - seeded from what WP2's scrubbing removes, + because the pattern detectors alone cannot catch policy-relevant + content like internal handles once common hosts are allowlisted. The + stated guarantee is honest about the residual: semantic sensitivity + beyond the detector and denylist classes remains a human review + control exercised through the WP2 disposition process, not a scanner + promise), the maintained-source + manifest checker, and the gate manifest. +- The example harness, in eight explicit phases (secret resolution + alone cannot make the template valid: placeholder rejection also + covers the non-secret publisher fields `domain`, `cookie_domain`, and + `origin_url`): (1) parse the unchanged source template; (2) assert it + FAILS deploy validation for exactly the expected placeholder set; + (3) apply deterministic non-secret customization in memory, never by + editing the template; (4) assert the customized config passes deploy + validation with secret key names intact; (5) serialize and verify the + blob envelope; (6) resolve through a fake secret store; (7) assert + runtime validation passes post-resolution; (8) run each optional + block and profile through isolated positive and negative probes. + Enumerate every `[integrations.*]` + subtree (grouped by first-segment ID, nested tables in their parent) + and every `[auction.providers.]` entry - commented or active, + enabled or disabled - deserializing into the typed structs with + ignored-key detection, running `Validate::validate`, the profile + compilers, and the deploy/startup checks via per-ID isolated `Settings` + fixtures with the integration/provider forced enabled (the runtime path + skips disabled blocks); valid and invalid compiled probes per profile. +- Inventory equality tests live where visibility allows: module-local + `#[cfg(test)]` tests inside core assert set equality for the private + registries (builders, plan registrations, profile registry, mediator, + JS module sets - replacing the deploy-ID constant's one-directional + subset check) against the same checked records the tool renders from. + Capability parity is behavioral, not ID-level: the tests instantiate + each integration under the predicate fixture matrix and inspect the + resulting registration state (proxy routes, rewriters, injectors, + post-processors, request filters, JS mode), comparing observed + capabilities against the record - so a record claiming a capability + after its `with_*` call changes fails. APS plan-dependent registration + and the DataDome filter are asserted in both predicate states. In the + capability and adapter-support records, executable columns (fan-out, + routes, status behavior) are test-backed; "operational support" and + "release status" are manually owned fields with a named owner and + review date, never implied to be code-derived. + +WP8b (lands last): + +- Wire everything as blocking CI: rustdoc matrix, native doctests (with + pinned Node), generated-region clean-diff, snippet manifest, scanner, + jsdoc lint, gate-manifest check across every surface in its mode, + repo/orphan/tombstone inventory, disposition-set equality, + maintained-source manifest equality. +- Link enforcement per set: VitePress covers only the built public set + (and after `srcExclude`, nothing internal), so `docs-parity` adds a + repository-relative path and anchor check over the active repo and + maintained internal sets; the scheduled external-link check's input + is all three active sets; each set gets its own synthesized dead-link + negative fixture. +- Workflow edits: CodeQL `rc/*` PR triggers; `.tool-versions` in + deploy-docs paths; normalized setup-node cache keys (all workflows, to + lockfiles); Dependabot roots (github-actions, browser and Next.js + fixture npm, docs-parity cargo); Wrangler pinned in `.tool-versions` + (the same pin consumed locally and in CI); the scheduled external link + check using a checker pinned by action SHA (versions chosen from + current stable at implementation and recorded where pinned; + job-scoped `issues: write`, dedup, auto-close, named owner, + fixture-tested); + `[lints] workspace = true` for openrtb-codegen. Where a governance + value can be asserted deterministically (a YAML-parsing static test + over workflow triggers, cache keys, Dependabot roots), it is; the + remainder is review-time evidence explicitly listed in the PR + description. +- `CLAUDE.md`/`AGENTS.md`/`TESTING.md`/`docs/guide/testing.md` gate + regions regenerated from the manifest in the same commit. + +Merge-blocking is a repository setting, not a workflow property: a +failing check blocks merges only when the ruleset/branch protection +requires it. WP8b therefore carries an externally owned acceptance item +recording: the required check names and their GitHub App, the +ruleset/branch-protection evidence for BOTH `rc/202608` and `main`, +the bypass policy, one demonstrated failing check actually preventing a +merge, and `merge_group` triggers on the new workflows if the +repository adopts merge queues. + +Acceptance: every runtime gate has a synthesized negative fixture (dead +link, broken intra-doc link, failing doctest, invalid or unknown-keyed +example block including a disabled integration table and a bad +`profile_config`, planted non-allowlisted domain, unclassified or +expired-waiver snippet fence, missing scoped JSDoc, inventory change +without regenerated region including a macOS-only CLI divergence, missing +README or unlisted orphan, gate mismatch in any manifest surface, removed +ownership marker); the static workflow assertions pass; regenerating all +regions and goldens at final HEAD produces no diff. + +## Sequencing + +| Order | Package | Size | Depends on | +| ----- | -------------------------------------- | ---- | ----------------------------------------------------------------------------------------------- | +| 0 | WP1 containment subset → `main` PR (b) | XS | - | +| 0b | `main` automation PR (c) | XS | WP8a workflow content; dispatch acceptance vs the rc PR head SHA | +| 9b | Post-merge activation PR (c2) → `main` | XS | #1049 merged into rc; own audited_main_tip; dual-checkout validation; real scheduled run | +| 0c | CNAME PR (d) | XS | open question 2; completion gate, does not block other rows | +| 10 | Release handoff PR (e) → `main` | XS | gates rc deletion (merge e, verify, then delete); issue + two runbooks produced by this refresh | +| 1 | WP1 hygiene (full, rc PR) | S | - | +| 2 | WP8a scaffolding | L | - | +| 3 | WP2 truth pass | M | WP8a (manifest, scanner) | +| 4 | WP3 config reference | L | WP8a (extractor, harness) | +| 5 | WP4 API reference | M | WP2, WP8a | +| 6 | WP5 pages + nav | L | WP2, WP3, WP8a | +| 7 | WP6 root + READMEs | M | WP2 | +| 8 | WP7 in-code docs | M | - | +| 9 | WP8b gate activation | M | WP2-WP7 | + +## Verification + +Verification runs in three epochs, because a single state cannot hold +both "target still equals the audited baseline" and "#1049 has merged": + +**Epoch 1 - pre-merge, at the final rc-PR HEAD** (`origin/rc/202608` +still equals audited_target_tip `07dfc1c6d`): all GitHub checks green (CodeQL with rc +triggers, format including the docs build, the seven test.yml jobs, the +four integration-test jobs, release builds, JS build/test); the WP8 +parity suite and negative fixtures green; regeneration produces no diff; +`cd docs && npm run lint && npm run format && npm run build`; the rustdoc +matrix locally; acceptance greps over the defined sets with output in the +PR description; the four adapter first-success smokes (Axum env bridge, +Fastly local push + secrets, Cloudflare envelope transfer, Spin local +push + variables) executed as documented with commands and cleanup +recorded; the `main` containment PR merged with its positive smoke, the +`main` automation PR merged with a successful read-only validation +dispatch against the rc PR head and validated Dependabot config, and the +CNAME PR (d) merged with its branch-specific live smoke (each `main` +PR's audited_main_tip assertion having passed); every follow-up filed with a recorded URL or +disposition; the release-handoff work (e) existing as a tracked issue +plus two reviewed runbooks (retarget / disable-and-remove), with its +owner and both path conditions recorded and branch deletion documented +as gated by (e); and the exact-tip baseline assertion for +`origin/rc/202608` (equal to the recorded audited_target_tip, contained +in the branch) passing at this HEAD - not a merge-base comparison. + +**Epoch 2 - after #1049 merges into rc**: record a `merged_rc_tip`; +dispatch that exact SHA through the read-only validation job and record +the result; open and merge the c2 activation PR to `main` (its own +audited_main_tip; dual-checkout validation - trusted tool from the +merged rc tip, files under test from the c2 head - covering the actual +changed workflow and Dependabot file); then observe one successful real +scheduled run, including the schedule-only issue job. + +**Epoch 3 - at release, before rc deletion**: verify rc landed on +`main` (or that it is being abandoned); open the concrete (e) PR from +the matching runbook, validated the same dual-checkout way against its +real diff; merge it; re-verify the retargeted (or removed) schedule and +Dependabot roots, live Pages containment, and the selected CNAME +behavior; only then is rc deleted. If the release merge happened, +`main` now carries the tooling, so the deferred required-check +activation on `main` is completed here per the WP8b protection item. + +## Open questions + +1. `fastly.toml` `service_id` allowlist owner and review date (blocks + WP8a scanner activation); the ops migration itself blocks nothing. +2. CNAME: delete (recommended) or custom domain (fully specified branch). +3. `FAQ_POC.md`: three-way - retire, archive, or factual rewrite + (owner: the maintainer driving this refresh; decide before WP2 + starts). Deterministic fallback if undecided by then: archive under + the historical tree, leaving no active-set page and no route to + preserve. "Do not retire" alone is not an answer - it selects + between archive and rewrite, and rewrite has its own acceptance. The gam/kargo + tombstones are NOT part of this question - WP2 applies them + unconditionally, with routes preserved, sidebar entries removed, and + old-route smokes asserting the tombstones serve. +4. `business-use-cases.md`: CLOSED - excluded with a source-level + banner; republishing is a separate future effort (see Non-goals). +5. CHANGELOG release cut (out of scope; deterministic no-release edit + defined in Non-goals). +6. Governance ownership (CODEOWNERS/minutes). Terminal disposition: if + no owner is named by the time WP6 lands, the fallback executes - the + governance document is corrected to current evidence (no minutes + exist; releases are not continuous) without adding CODEOWNERS or + minutes commitments - and the refresh completes; naming owners + remains a follow-up for maintainers. +7. Delivery shape confirmation (blocks starting implementation). +8. CodeQL `push` coverage for `rc/*` (non-blocking). + +## Follow-up issues to file (code, not docs) + +Filing is owned work, not an aspiration: WP8b deduplicates this list +against the existing tracker and files each item (or records an +existing-issue disposition), with issue URLs, owner, and labels recorded +in the rc PR description; final verification checks that every entry +below has a URL or disposition. + +- `Hooks::stores()` unimplemented on Cloudflare, Spin, and Axum: request- + time config/KV registries are empty, the declared `TRUSTED_SERVER_KV` + binding is never opened, Spin's request-signing kid variables are + unreachable, and `cloudflare.toml` is dead config - wire `stores()` or + retire the manifests and the stale rustdoc claims (docs fix the rustdoc + in WP7 either way). +- The Cloudflare Worker does not read the config store `ts config push` + writes (nested-var startup only), and the CLI has no envelope + export/output flag - wire the store read or add the export so the + documented bridge becomes unnecessary. +- `ts serve --adapter axum` does not consume the local config store the + push writes; the env-var bridge is the documented path. +- Cloudflare registers no `/health`; startup-failure status/health + behavior differs per adapter (Spin 503 + live health, others 500) - + decide a uniform contract. +- `imp_ext` reserved-field protection: docs claimed it, code guards only + `request_ext` - decide whether to guard `imp_ext`. +- `ec.partners[*].ts_pull_token` template placeholder + (`replace-with-partner-ts-pull-token`) is in no rejection constant + list. +- `trusted_client_ip.shared_secret` is `Redacted` but inline in the + blob (not in `secret_fields()`) - confirm intended or migrate to a + store reference. (`tinybird.access_token_secret` is accepted and + discarded at normalization; its only fix is documentation.) +- The deploy-ID constant is `#[cfg(test)]` with a one-directional subset + check (a stale extra entry passes) - superseded by the WP8a + set-equality test, but the constant itself should be fixed or removed. +- Vendored `edgezero-cli` help text leaks internal spec references into + `ts config push --help`; fix upstream and bump the pin. +- Tinybird access-log telemetry config present but rejected at runtime; + auction emission Fastly-only. +- `.env.dev` references an `opid_store` that `fastly.toml` does not + declare. +- Fastly `--staging` deploys load the PRODUCTION config blob (the entry + point does not read the `edgezero_runtime_env` store-name selectors); + rc's cli.md documents it as a known limitation - file the code fix. + +## Appendix A: Route inventory (regenerated at `a163367b3`) + +Sources: `adapter-fastly/src/{main,app}.rs`, `adapter-axum/src/app.rs`, +`adapter-cloudflare/src/app.rs`, `adapter-spin/src/app.rs`. Symbols cited; +line numbers are hints. WP8 snapshots record response semantics for +guarded/unsupported routes. `trusted_client_ip` is middleware (sanitize +outermost on Axum/Cloudflare/Spin; Fastly sanitizes in `main.rs`; only +Fastly resolves the client IP from it). Spin adds an innermost +`NormalizeMiddleware` (spin-header derivation). + +| Route | Methods | Fastly | Axum | Cloudflare | Spin | +| -------------------------------------- | ------------------------------------ | ------------------------------------ | ------------- | ------------------------------- | ---------------------------------- | +| `/health` | GET | pre-router, survives startup failure | real | **absent** (falls to publisher) | real; also alive in the 503 router | +| `/_ts/debug/ja4` | GET | pre-router, config-gated | - | - | - | +| `/.well-known/trusted-server.json` | GET | real | real | real | real | +| `/verify-signature` | POST | real | real | real | real | +| `/_ts/admin/keys/{rotate,deactivate}` | POST | real | 501 | 501 | 501 | +| `/_ts/admin/ec`, `/_ts/admin/ec/{id}` | GET | real (EC KV) | not-supported | not-supported | not-supported | +| `/_ts/admin/eids` | GET | real | real | real | real | +| `/admin/keys/*` (legacy) | 7 methods | 404 deny | 404 deny | 404 deny | 404 deny | +| `/_ts/api/v1/batch-sync` | POST | real (Bearer + rate limit) | - | - | - | +| `/_ts/api/v1/identify` | GET, OPTIONS | real | - | - | - | +| `/_ts/set-tester`, `/_ts/clear-tester` | GET | real (gated) | - | - | - | +| `/auction` | POST | real | real | real | real | +| `/_ts/page-bids`, `/__ts/page-bids` | GET; OPTIONS denied in-handler | real | real | real | real | +| `/first-party/{proxy,click}` | GET | real | real | real | real | +| `/first-party/{sign,proxy-rebuild}` | GET, POST | real | real | real | real | +| `/static/tsjs=` | GET (fallback chain) | real | real | real | real | +| `/integrations//...` | per the route-family inventory below | real | real | real | real | +| asset route prefixes | GET, HEAD | Fastly only | - | - | - | +| publisher fallback | 7 explicit methods | real | real | real | real | + +Integration route families (the checked inventory WP4 generates from; +disabled integrations register nothing and their paths fall through to +the publisher proxy; a duplicate route registration is a startup +configuration error, not silently resolved): + +- Fixed: `prebid` `GET /integrations/prebid/bundle.js`; `aps` + `GET /integrations/aps/renderer` (conditional: only in + `trusted_server` rendering mode); `testlight` + `POST /integrations/testlight/auction`; `didomi` configurable-prefix + loader/api routes; `datadome` `tags.js`, `/js/`, `/js/*`; + `google_tag_manager` `gtm.js`, `gtag/js`, `gtag.js`, `collect`, + `g/collect`; `gpt` `script`, `pagead/*`, `tag/*`; `sourcepoint` + `cdn/*`; `permutive` `api/*`, `secure-signal/*`, `events/*`, `sync/*`, + `cdn/*`, `sdk`; `lockr` `sdk`, `api/*`. +- Prefix-overridable: the trait default `/integrations/` can be + overridden by an integration's implementation; the operator- + configurable case is Didomi's `proxy_path` config field. +- Config-derived: `prebid` additionally neutralizes operator-configured + `script_patterns` plus `/static/prebid/{*rest}`. +- Methods are per-registration (GET for scripts/CDN, GET+POST for + beacon/api paths); the WP8a route snapshot records each. + +Startup failure: Spin installs a hardened 503 router (generic body, +`/health` 200, all fallback methods); Cloudflare and Fastly serve 500 +from the error router; the Cloudflare and Axum degraded routers answer +the underlying error status on every path (no `/health` carve-out). EC partner API, tester cookies, and JA4 remain Fastly-only +(they need platform KV and entry-point wiring). Fastly-only capabilities: +asset routes, image optimizer, request filters (DataDome pre-route), +Tinybird emission. Provider fan-out: Fastly and Axum allow multiple +enabled providers; Cloudflare and Spin reject them at startup (dormant +multi-provider configs are accepted when `auction.enabled = false`). + +## Appendix B: Settings inventory (17 roots at `a163367b3`) + +`Settings` is `deny_unknown_fields`; `Option` roots: `trusted_client_ip`, +`request_signing`, `creative_opportunities`. + +| # | Root | example.toml | configuration.md | +| --- | -------------------------------- | ------------------------------- | -------------------- | +| 1 | `publisher` | active | yes | +| 2 | `tester_cookie` | commented | yes | +| 3 | `trusted_client_ip` (Opt) | commented | yes | +| 4 | `ec` | active (partners commented) | yes | +| 5 | `integrations` | mixed (5 active stubs) | 5 of 14 subsections | +| 6 | `handlers` | active | yes | +| 7 | `response_headers` | commented | yes | +| 8 | `request_signing` (Opt) | commented | yes | +| 9 | `rewrite` | commented | yes | +| 10 | `auction` (+ providers, bidders) | active incl. `pbs-main` | yes (config-first) | +| 11 | `consent` | commented | **missing section** | +| 12 | `cache` | commented rules | yes | +| 13 | `proxy` | header active, leaves commented | yes | +| 14 | `creative_opportunities` (Opt) | active | yes | +| 15 | `image_optimizer` | commented | nested under Proxy | +| 16 | `tinybird` | commented | **Quick Start only** | +| 17 | `debug` | commented (+ comment options) | **missing section** | + +The docs "Key Sections" table lists 10 of 17. Secret model: 11 +`secret_fields()` paths, all `KeyInDefault` (3 required: +`publisher.proxy_secret`, `ec.passphrase`, `handlers[*].password`); +resolution flow is verify → `remove_inactive_secret_references` → +`resolve_secret_references` → `from_json_value` → +`validate_settings_for_runtime`; deploy validation excludes secret-leaf +attributes and requires key names conditionally +(`validate_proxy_secret_strength` was removed in the audited delta - no +runtime length enforcement on the resolved proxy secret remains). Redacted-but-inline: +`trusted_client_ip.shared_secret` only; `tinybird.access_token_secret` +is accepted-but-normalized-away (set to `None`, never serialized). +Directional dispositions (WP3) classify every path. +Template placeholder constants and their consumer surface are +load-bearing; the WP8a-enumerated consumer manifest is canonical (it +distinguishes literal-substitution consumers - config.rs template tests, +CLI ad_templates and audit generate/validate substitutions, the +template-cache script - from include-only consumers such as +`config init`), and this appendix does not restate its count. Validator inventory for +companion entries: 2 struct-level schema validators +(`validate_trusted_client_ip`, APS inventory-identity override), the +field-level custom-validator sites (enumerated canonically by the WP8a +companion manifest, which fails closed on unclassified validators, +rather than by a count here), the imperative +`finalize_deserialized` pipeline (normalize → prepare_runtime → derive +validate → admin coverage → placeholder rejection), the plan-compiler +family (`ProviderId`/`BidderId` grammars, `canonicalize_endpoint`, +notification caps, mediator match, signing gate, +`validate_for_target`), the profile compilers (standard: 16KiB / +depth-8 / 256-key extension limits, `request_ext`-only reserved fields; +prebid-server: override-rule engine; aps: account and inventory +validators), and the integration deploy/startup pairs (DataDome split, +prebid browser-ownership cross-check against the plan, PartnerRegistry +deploy/runtime). + +## Appendix C: Integration and provider inventories (at `a163367b3`) + +Named sets and counts (set-equality tested in WP8a): + +- Deploy-validated IDs: **14** (`validate_enabled_integrations`; the + `#[cfg(test)]` constant currently checks subset one-directionally). +- Registry `builders()`: **11** - testlight, nextjs, permutive, lockr, + didomi, sourcepoint, osano, google_tag_manager, datadome, gpt, + gpt_diagnostics. +- Plan registrations: **2** - `prebid::register_for_plan` and + `aps::register_for_plan`, prepended by `IntegrationRegistry::with_plan` + (builders take only `&Settings`; these two need the compiled + `AuctionPlan`, the single config→runtime boundary shared with the + orchestrator via one `Arc`); APS registers iff + `plan.has_profile("aps")`. +- Profile registry: **3** - `standard` (auction-inherited timeout), + `prebid-server` (1000ms), `aps` (800ms), in `PROFILE_REGISTRATIONS`; + provider instances are operator-defined `[auction.providers.]` + (`ProviderId` grammar `^[a-z][a-z0-9-]{0,62}$`; `profile_config` is a + raw JSON object discriminated by the sibling `profile` field, compiled + into the profile's typed `deny_unknown_fields` struct) flowing through + `GenericOpenRtbProvider`. +- Mediator: `adserver_mock` via `register_providers`, matched exactly to + `auction.mediator`; it never enters the integration registry. +- JS: **12** integration `index.ts` modules (13 dirs; `aps` ships only a + render helper imported by core/prebid/gpt), **13** emitted bundles + (+core). `JS_ALWAYS = ["creative"]`. Three loading modes: bundled, + deferred (prebid only), standalone tag (`gpt_diagnostics`, which is + `.without_js()` in the registry but served via its own decision path). +- `IntegrationMetadata` omits post-processors, JS modes, and plan info - + the checked capability record (typed capability + config-predicate + conditions, fixture matrix evaluating both states) is the rendering + source; equality tests live module-local where the private registries + are visible. + +Capabilities (P proxy, AR attribute rewriter, SR script rewriter, HI head +injector, PP post-processor, RF request filter): prebid P/AR/HI + +deferred JS; aps HI always, P conditional on `trusted_server` rendering +mode, no JS bundle; testlight P/AR; nextjs SRx2/PP, no JS; permutive +P/AR; lockr P/AR; didomi P/HI; sourcepoint P/AR/HI; osano bare; +google_tag_manager P/AR/SR; datadome P/AR/HI + RF conditional on +`enable_protection`; gpt P/AR/HI; gpt_diagnostics bare + standalone JS; +creative JS-only, always injected. + +## Appendix D: CLI and per-adapter configuration handoff + +`ts` (the two-platform help union is canonical; macOS adds `dev proxy`): +`audit page|generate|ad-templates generate[--scroll]|verify`, +`active-version`, +`auth login|logout|status`, `build`, +`config init|diff|push|validate|gc|ad-templates lint|match|check|explain`, +`deploy`, `healthcheck`, `prebid bundle`, `provision`, `rollback`, +`serve`, `dev proxy [ca ...]`. `--version` is available. Drift-detecting +commands use a stable drift exit code. `config gc --store ` is owned +by the pinned `edgezero-cli` (newly documented, not newly added); the +deploy-lifecycle commands (`active-version`, `healthcheck`, `rollback`) +are Fastly-only per rc's own docs; cli.md documents a known limitation +that `--staging` loads the production config blob (follow-up below). +rc's cli.md already covers the lifecycle commands; the remaining work is +the generated-region conversion and description gating. + +Per-adapter configuration handoff (deployment-guide truth): + +- Fastly: config store `trusted_server_config` + secret store + `ts_secrets` (logical mapping via the `edgezero_runtime_env` config + store); `ts config push --local` mutates `fastly.toml`; the checked-in + store ships empty, so a bare `fastly compute serve` serves 500 on + non-health paths. +- Axum: config via + `TRUSTED_SERVER_CONFIG_TRUSTED_SERVER_CONFIG_TRUSTED_SERVER_CONFIG`, + secrets via `TRUSTED_SERVER_SECRET_{STORE}_{KEY}`. +- Cloudflare: `TRUSTED_SERVER_CONFIG` var = `{"app_config": ""}` (nested wrapper) + `wrangler secret put `; + push-written KV is unread by the Worker. +- Spin: blob in KV store label `default` (push requires + `EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME=default` or it + lands in the wrong store); secrets via encoded Spin variables + (`v__v_` encoding; empty defaults fail closed). + +Runtime environment variables and the `TRUSTED_SERVER__` CLI overlay are +documented as separate surfaces (WP2). + +## Appendix E: Still-open finding index (verified 2026-08-27) + +- `srcExclude` absent; 133 superpowers files in the CI-built site; empty + `guide/index.md`; nav Guide/Business Value links; CNAME placeholder. +- `fastly.toml:4,10,38` + inconsistent fixture labels; + `docs/package.json` ISC/not-private; onboarding published. +- `RequestWrapper` (`architecture.md:93-104`, + `.claude/agents/code-architect.md:16`); Equativ (`ad-serving.md:11`, + `.claude/agents/issue-creator.md:85`, `FAQ_POC.md`); `.with_asset` + (`creative-processing.md:808`, `integration-guide.md:84,248`); + `npm run type-check` (`error-reference.md:614`); + `settings_data::get_settings` (`configuration.md:2301`); auction README + rot; `SEQUENCE.md` links; `TESTING.md` runbook. +- `[consent]`/`[debug]` sections missing; `[tinybird]` Quick-Start-only; + Key Sections 10/17; reserved-field `imp_ext` docs/code mismatch; + integration subsections 5/14; `adserver_mock` stranded. +- CHANGELOG: 8 breaking entries (one `**Breaking:**`, seven + `**Breaking** -` variants), dead v1.2.0 links. +- Stale rustdoc: the Cloudflare `platform.rs:579-592` stores claim (and + its Spin sibling); `cloudflare.toml` dead; false workflow comments + (Spin release build, test-cli default target). +- Env files carry retired `TRUSTED_SERVER__SYNTHETIC__*` keys; + `opid_store` mismatch. +- Missing pages: cloudflare/spin/axum-dev/edgezero/telemetry/tsjs/ + adserver_mock guides; 7 crate READMEs; integration-guide snippets do + not compile. +- Enforcement gaps: no rustdoc/doctests in CI, no parity checks, jsdoc + inert, openrtb-codegen lints, PR template `tracing`, slash-command gate + drift.