Skip to content

ADR-0013 — The CMS worker fleet is the sole ship lane; the static assembler becomes internal tooling

Status: Proposed (drafted 2026-07-18; owner decision recorded same day, in-session) Date: 2026-07-18 Deciders: Cathal Dempsey Consulted: two independent blind structural reviews this session (deep-reasoner, Codex) — both flagged the lane divergence; one identified the corpus/lane incoherence this ADR resolves. Related: ADR-0008 (per-site worker fleet — confirmed and extended), ADR-0009 (measured near-match — its Slice 6 corpus vehicle is re-specced here), ADR-0003 (assembler-fulldev — its product role is superseded; the code remains as tooling), ADR-0004 (Path A adapters — unchanged), ADR-0011 (deterministic gate — unchanged)

Context

Two output lanes have coexisted since ADR-0003/0004: per-site static Astro builds (lib/assembler-fulldev) and the per-site single-tenant CMS workers (apps/cms, ADR-0008). The project docs still describe the static lane as the long-tail product ("per-site static builds for the long tail" — CLAUDE.md), while ADR-0008 records that all ~1800 sites may become self-service. The two stories imply opposite investment targets, and the ambiguity was never resolved — it was worked around.

Three facts forced the decision:

  1. Every ADR-0009 fidelity dimension shipped CMS-lane-only. Verified in code 2026-07-18: lib/assembler-fulldev/emit.js and translate.js consume no section-backgrounds.json and no makeStyleLookup; nav is still groupNavByPrefix() (which provably fails on WCP). The static lane renders the pre-ADR-0009 house style that ADR-0009 exists to replace.
  2. The Slice 6 corpus vehicle builds the wrong lane. scripts/run-fulldev-batch.sh builds static-lane output. Calibrating the ship/hold gate on a 30–50-site batch of it would fit thresholds against output the ship lane does not render — the calibration would be internally consistent and externally meaningless.
  3. The owner decision (2026-07-18): most clients will not edit their site — but every client must know they can. The capability is part of the product promise. Therefore every site ships with its own admin, i.e. a CMS worker per site.

Decision

  1. The per-site CMS worker fleet is the sole ship lane. Every replatformed site in the Astro lane ships as ADR-0008 specifies: its own worker, its own D1, its own Access app, sharded across accounts. No client site ships from the static assembler.

  2. The static assembler is demoted to internal tooling. The code stays (its crawl inputs, theme emission, and the canonical block library all remain load-bearing for the ship lane), but it receives no further fidelity investment and nothing it emits is client-facing. Consequence for standing deferrals: every known-issues item gated on "the first per-site static build intended for launch" (static-lane capture consumption; static-lane nav grouping; team-grid/checklist static-lane eyeball; the static noindex launch blocker) is void — that trigger can no longer fire. Closures land at acceptance.

  3. The Slice 6 corpus must score the CMS lane's render. The batch, per site: crawl → seed (seed-from-build + transformer, capture stamps included) → image migration (ADR-0005 path, per-domain R2 prefixes) → local per-site CMS build + serve (no worker deploy, no production writes) → symmetric ours-capture → scorecard. The runner implementing this is the next slice — scope: scope-2026-07-18-cms-batch-runner.md. run-fulldev-batch.sh remains available as internal tooling but is no longer the corpus vehicle.

  4. Self-service editability is confirmed as a product promise. "They need to know they can" only holds if an edit actually persists. The emdash save race (known-issues, reproduced on 0.28.1) is therefore a product blocker, not an ops annoyance: file the already-drafted upstream repro, timebox an owned mitigation (serialize the admin save path via patch-package or equivalent), and if neither lands, this ADR's premise must be re-examined openly — not silently carried.

Consequences

Positive

  • One topology for the whole portfolio: no per-tier fidelity split, no static→CMS migration machinery, no second render surface to keep honest.
  • The ADR-0009 investment concentrates where every site ships; the corpus measures the shipped artifact, so the Slice 6 fit means what it claims to mean.
  • ADR-0008's "N of everything" cost stops being a hedge and becomes a plannable certainty.

Negative / risks

  • Slice 3 provisioning automation + cross-account proof are now hard prerequisites for scale — unchanged in content from ADR-0008, changed in certainty. Both remain unstarted/unproven at drafting.
  • Fleet redeploy fan-out is a fact of life: a shared block fix means ~1800 worker redeploys. The loop is trivial; it must exist before the fleet does.
  • Verify early — per-account limits and pricing wherever the fleet multiplies them: Workers script count (~500/account, known), D1 database count/storage, and especially Cloudflare Access / Zero Trust seats at ~450 owner logins per account. Unverified at drafting; fold into the ADR-0008 Slice 3 second-account spike. Cheaper to learn at site 3 than at site 500.
  • The static lane's sunk product cost is written off. Bounded: the pipeline underneath it (dom-pipeline, capture, theme, matchers, canonical blocks) is the same pipeline the ship lane consumes.

When to revisit

  • Access/D1/Workers economics or limits fail at fleet scale → Workers-for-Platforms dispatch namespaces (ADR-0008's stated fallback) lift the script ceiling; re-evaluate account sharding wholesale.
  • The save race proves unfixable upstream AND unpatchable locally → the editing promise cannot be honoured as specced; re-open the lane/tier question rather than shipping a door that doesn't open.
  • A client segment materialises that explicitly wants no admin → a static tier may return as a deliberate product decision with its own fidelity plan — never as a silent default.

On acceptance (checklist)

  • Pointer note in ADR-0003: product role superseded by ADR-0013; code remains as internal tooling.
  • Pointer note in ADR-0009: Slice 6 vehicle re-specced (§Decision 3); "both lanes" phrasing scoped to "canonical library shared; ship lane is CMS".
  • Note in ADR-0008: "all ~1800 may become self-service" hardens to "every site ships with its own admin".
  • CLAUDE.md project description corrected (the "per-site static builds for the long tail" line).
  • known-issues: close/annotate the void static-lane deferrals (Decision 2 list).
  • mkdocs nav entry (added at drafting).

References

  • ADR-0003 / ADR-0008 / ADR-0009 (the three records this ADR reconciles)
  • lib/assembler-fulldev/{emit,translate}.js (verified capture-blind, 2026-07-18)
  • scripts/run-fulldev-batch.sh (static-lane batch, EC2-bound)
  • known-issues: "Per-site static lane consumes no section-background capture", "The per-site static lane's nav is still URL-prefix-grouped", the emdash save-race entry
  • Memory: per_site_worker_model (one single-tenant worker per site, all 1800 may self-manage — now hardened by this ADR)