drogna

A demonstration harness. Every number in it is invented.

ADR 0031 — The site is built in TypeScript, and the estate has two tenants

Status: Accepted, 29 August 2026. Supersedes ADR-0010 (V1 site tooling). Date: 29 August 2026 Requirements: SRD-v2 PR-04, PR-04a, NFR-04, NFR-05, PR-01; plan D16, D18, D19

Context

The published site was V1's, in three senses. Its tooling was MkDocs with mkdocs-material, pinned in site/requirements.txt — a second language runtime, which NFR-01 and NFR-05 now forbid outright. Its content described twelve containers and eighteen components, none of which run any more; its landing page said "Nothing yet, other than this site." And its publication gates — five Python scripts checking links, external resources, the FR-01 statement, indexing and the documentation manifest — ran only inside .github/workflows/pages.yml, which the V2 retirement commit deleted. They had therefore been checking nothing for the whole of V2's implementation, and a tree where nothing checks the site looks exactly like a tree where the site is fine.

Meanwhile the estate on gh-pages had gained a second tenant. instances.yml publishes a per-branch build of the app under instances/<slug>/, retained after its pull request completes (NFR-04), so that a review comment can deep-link a specific build at a specific view (D16, FR-15). Nothing listed them: the addresses existed and were discoverable only by already knowing them. And nothing had yet reconciled that tenant with the V1 site sitting at the estate root, which a V2 publication would have to replace without touching it.

Decision

One. The site generator is TypeScript, under scripts/site/, run by pnpm site:build. Markdown is rendered by markdown-it, a build-time dependency in the same category MkDocs occupied — it never ships to a reader, and what reaches the estate is HTML with no script and no fetched sub-resource. The corpus was surveyed before the renderer was written (no footnotes, no mathematics, no definition lists, admonitions of exactly two kinds), so what is implemented is what the content uses. site/mkdocs.yml, site/requirements.txt, site/overrides/, site/hooks/, site/tools/ and site/gates/ are deleted. No Python remains under site/.

Two. The publication disciplines become four gates in scripts/gates.registrycheck-site-links, check-site-resources, check-site-publication, check-site-disclosure — so they run in pnpm gates, in pnpm check and in CI, not only in a publishing workflow. Each builds the site into memory and inspects it, so no build step has to have run first. Each was watched failing on a planted violation before it was trusted.

Three. Two faults are removed rather than detected. Navigation is derived from the directory tree, so a page cannot be missing from it (V1 kept the navigation in mkdocs.yml and had --strict fail the build when a page was left out). The Version 1 banner is a property of sitting under archive/, so it cannot be forgotten on a page and cannot be left on one that moves out.

Four. What carried, what was archived, what was retired:

Carried The glossary and the component reference as first-class pages (D18); the standards primers and algorithm derivations, which are true of V2 unchanged; the decision records, published from docs/adr/ with each status read out of the record; the no-external-request, noindex and FR-01-statement disciplines.
Archived The seventeen V1 blog entries; the eighteen-component subsystem reference; the V1 architecture overview. All published, all bannered, all reached only from archive/.
Retired The MkDocs toolchain and the Python gates; site/docs/decisions/site-tooling.md, which was a pointer to ADR-0010; docs/manifest.yaml; the OCR pass over published images.

Five. The component reference is generated from contracts/topology.json — the same master the app is built against and the topology-drift gate rebuilds — rather than written. V1's eighteen hand-written pages went stale twice, first claiming no code existed long after it did, then describing retired software. The generated reference cannot be wrong about which component may publish what; it can still be incomplete about what a component is for, and that is what the prose beside the tables is.

Six. The estate has two tenants and the boundary is enforced in code. The site owns the root; instances/ is owned by instances.yml. scripts/publish-site.ts records a manifest of what it published and, next time, removes only those paths — and refuses to write or reap under another tenant's path even when handed a manifest that claims one. scripts/estate-index.ts regenerates instances/index.html from what the estate actually holds, so retained instances of completed branches stay listed. Each deployment writes an instance.json beside its build recording ref, commit and date; instances published before that file existed are listed saying their provenance is not recorded, rather than dropped or given a plausible blank.

Consequences

The site is buildable, checkable and previewable by anyone who has run pnpm install, with no second toolchain, and the disciplines that guard it are now part of the same check every other rule runs under. A gate can no longer be orphaned by deleting a workflow.

docs/manifest.yaml is retired rather than ported. Every gate that read it is deleted; its page list names V1 pages that are now archived, its component list names C-01 to C-21, and its one load-bearing entry — adrs.published — is now unconditional and covered by a test asserting that every record under docs/adr/ is published. What is lost is the word-count floor machinery, which measured stubs by length; the part of it that did the work, that a page declaring itself a stub is a stub whatever its length, is kept as an assertion in check-site-publication. Should stubs reappear in quantity, the floors are worth rebuilding in TypeScript, and the reasoning that derived them from what was on disk is in this repository's history.

One check is genuinely lost rather than ported. V1's site gate ran OCR over every published image, hunting an address bar or a host path burnt into a screenshot — the one place text reaches a reader that no source scan can see. An OCR engine is a second language runtime, which NFR-01 forbids outright, so it goes. What replaces it is not another check but a discipline that was already there: every screenshot comes from the capture mechanism under scripts/capture/, capture output is committed deliberately rather than swept in, and the entries that matter embed a running instance instead of a picture of one. The text half of that gate is carried as check-site-disclosure, which reads the published pages for an email address or a home-directory path.

Its tracked-entity noun scan is deliberately not carried, and that is an inheritance rather than an omission: V1 ruled that documentation must be able to discuss a prohibition in order to state it, excluded the site from that scan, and recorded the reason. Seven lines of this site would match today and all seven are sentences about the rule. check-vocabulary still scans everything that is not documentation.

The V1 site's URLs change. /subsystems/c13-model-runner/ is now /archive/subsystems/c13-model-runner/, and the V1 entries move under /archive/blog/. The alternative was leaving the V1 tree in place beneath the V2 one, which would have published two contradictory sites at overlapping addresses; the content is kept, the addresses are not. The first publication finds no manifest — V1's estate was written by a tool that kept none — and that case is treated as the migration it is: everything at the root that is not another tenant is replaced.

Only the default branch publishes the site. A branch's app is reviewable as its own instance, which is what review needs; a branch's site is reviewable by building it.

Alternatives rejected

Keep MkDocs. It works, the theme is good, and the corpus is written for it. It is also Python, and NFR-01's whole claim is that no step of building, testing, gating or demonstrating the system needs a second language runtime. A site build is a step. The cost of the rewrite is a renderer that supports what this corpus uses rather than everything Python-Markdown does; the cost of keeping it is that the claim becomes false, with an asterisk.

Port the Python gates one for one. Two of them — the manifest gate and the subsystem coverage gate — check a document structure that V2 does not have. Porting them would have meant keeping docs/manifest.yaml alive to give them something to read.

Leave the V1 site published beside the V2 one, at its own addresses. Attractive, because D18 says the estate grows rather than being rebuilt. Rejected because the site is one tenant, not two: two sites at overlapping roots would disagree, and the reader has no way to tell which is current. D18's concern is that a review instance not wait for a merge, and that instances stay addressable; both hold, and are enforced.

Hand-write the V2 component reference. It reads better. It is also the thing that went stale twice, and the second time it went stale it was describing software that had been deleted.