# ADR: Docs and testing

Status: accepted, 2026-09-01. A full review of the ADR set and the test
suites is planned future work; this record captures the decisions as they
stand so nothing is lost before that review happens.

## In plain words

This page records how the project is documented and tested. The documents are served from the running site's menus, so a reader never has to open the code, and every release must first pass the three test suites, including a browser test for every menu of documents.

What that is worth: a developer finds each decision in one place and knows a green run means the same thing on a laptop as in a deploy, and the organization can check the site's claims against a picture or a test.

## Context

The project doubles as a portfolio piece. The decisions about how it is
documented and tested accumulated across the first week of building, in
chat and in session logs, and only some of them had made it into decision
records. This ADR closes that gap.

## The documentation decisions

- Docs are served by the running app, from header menus, so everything is
  exposed without opening the repository. The site tells its whole story
  standalone.
- The audience is a busy reviewer or a person trying to learn. Plain
language comes before jargon every time.
- Menus are organized parent over children. About holds the project docs
  and the resume. Hosting holds the hosting overview with its ADRs and the
  Bicep file. CI/CD holds the pipeline story. Best Practices holds the
  practice record with its ADRs. A new decision gets a record and a menu
  home the day it lands.
- Real configuration screenshots are embedded in the docs, captured from
  the actual dashboards, so every claim can be checked against a picture.
- The production design stays documented and undeployed: main.bicep is
  served in the Hosting menu as how this would be hosted in production.

## The testing decisions

- Three suites gate every ship: the .NET API tests, the vitest unit tests,
  and the Playwright end-to-end checks. A red suite stops the pipeline
  before any image is built.
- Every doc menu gets a Playwright spec proving the menu opens its
  documents in the running app. Docs are features here and get tested like
  features.
- The suites run locally through the same scripts that ship, so a green
  run means the same thing on a workstation and in a deploy.

## Consequences

- Decisions stop living only in chat transcripts and session logs.
- The planned review has a single starting point: this record plus the
  five ADRs before it.
- The cost is accepted and known: every new surface carries a doc, a menu
  entry, and a spec, which slows a ship by minutes and is worth it.

## Addendum, 2026-09-09: the drawing carries the two sites

The infrastructure drawing was redrawn by hand again for the morning's
change (ADR: A permanent address for the second site): the visitor's box
names both sites, the DNS box carries both CNAME records, the edge box says
the Host header picks the origin and names the two rules above the
catch-all, and the "both stores" box says the second group answers behind
the same edge at its own name and that the Store bar links each site to the
other rather than setting a cookie. A fourth drawing, TheYard's two sites,
shows that path on its own at `/api/docs/diagrams/two-sites`, drawn by
`docs/images/two-sites.mjs` in the same style. The PNG beside each SVG is
rendered by `docs/images/render.mjs` in the repository's own Chrome, as
before.

## Addendum, 2026-09-09: the provenance stamp, and the test that keeps it out of the source

The redrawn infrastructure source reached the developer's machine eight
kilobytes heavier than it was drawn. One of the tools that carries a file
from the assistant's workspace to that machine writes a C2PA content
credential into every image it touches: a base64 manifest inside a
`<metadata>` element and a namespace on the root element, signed by
Anthropic and reading "Claude provided this file at the request of a user
and may have created or modified the file contents". In a photograph that is
a few kilobytes nobody sees. In this SVG it was half the file, it is not
part of the drawing, and the diagram page at
`/api/docs/diagrams/infrastructure` inlines the whole file into its HTML on
every visit, so every reader would have downloaded a signature block to look
at a picture.

The stamped copy was replaced before it shipped: with the two insertions
removed, the file's checksum matched the source as drawn, byte for byte, and
the PNG was re-rendered from it. `DiagramPageTests` now reads every SVG under
`docs/images` and fails on a `<metadata>` element or a C2PA mark, which turns
the next stamped copy into a red gate instead of a file size that looked
wrong. The mechanism, for whoever meets it next: a drawing written to the
repository by that route is read back and compared with what was sent, and
the copy that goes in is the one the repository's own shell wrote. The test,
read from this build:

```csharp
/// <summary>
/// A drawing's source is what its author drew and nothing else (ADR-006,
/// the addendum on the provenance stamp). One of the tools that carries a
/// file from the assistant's workspace to the developer's machine writes a
/// signed content credential into every image it touches: a base64
/// manifest in a metadata element and a namespace on the root. In a
/// photograph that is a few kilobytes nobody sees; in an SVG it was half
/// the file, and the diagram page inlines the whole file into its HTML on
/// every visit. Every SVG the repository keeps is read for it here, so the
/// next stamped copy is caught by the gate and not by a file size that
/// looked wrong.
/// </summary>
[Fact]
public void Every_drawing_is_the_source_as_drawn_with_no_stamp_written_into_it()
{
    string images = Path.Combine(Repo.Root(), "docs", "images");
    var drawings = Directory.EnumerateFiles(images, "*.svg").OrderBy(path => path).ToList();
    Assert.NotEmpty(drawings);

    var stamped = drawings
        .Where(path =>
        {
            string source = File.ReadAllText(path);
            return source.Contains("<metadata", StringComparison.Ordinal)
                || source.Contains("c2pa", StringComparison.OrdinalIgnoreCase);
        })
        .Select(Path.GetFileName)
        .ToList();

    Assert.True(
        stamped.Count == 0,
        $"stamped on the way in, strip and re-copy: {string.Join(", ", stamped)}");
}

/// <summary>
/// The same rule for the photographs (ADR-006, the second addendum on the
/// provenance stamp). The SVG check above shipped on 9 September and the
/// eighteen PNG and JPEG screenshots that had arrived by the same route
/// were noted and left, on the argument that a credential in a photograph
/// changes nothing on the page. The owner's answer was that a signed
/// machine-provenance credential on his images is exactly the signal he
/// asked to have removed, so they were stripped on 13 September and this
/// keeps them that way. In a PNG the manifest is a JUMBF box in a caBX
/// chunk; in a JPEG it is a JUMBF box across APP11 segments. Both carry the
/// same marks, so one scan reads every raster image for them.
/// </summary>
[Fact]
public void Every_photograph_is_the_capture_as_taken_with_no_stamp_written_into_it()
{
    string images = Path.Combine(Repo.Root(), "docs", "images");
    var photographs = Directory.EnumerateFiles(images)
        .Where(path => Path.GetExtension(path).ToLowerInvariant() is ".png" or ".jpg" or ".jpeg")
        .OrderBy(path => path)
        .ToList();
    Assert.NotEmpty(photographs);

    var stamped = photographs
        .Where(path => CarriesAStamp(File.ReadAllBytes(path)))
        .Select(Path.GetFileName)
        .ToList();

    Assert.True(
        stamped.Count == 0,
        $"stamped on the way in, strip through the repository's own shell: {string.Join(", ", stamped)}");
}

/// <summary>
/// The marks a content credential leaves in a raster file: the JUMBF box
/// types, the C2PA manifest label and the PNG chunk that carries them.
/// Bytes rather than text, because a JPEG is not text and a credential's
/// label is ASCII either way.
/// </summary>
private static bool CarriesAStamp(byte[] bytes)
{
    ReadOnlySpan<byte> span = bytes;
    foreach (string mark in new[] { "c2pa", "jumb", "jumd", "caBX", "urn:c2pa" })
    {
        if (span.IndexOf(System.Text.Encoding.ASCII.GetBytes(mark)) >= 0)
        {
            return true;
        }
    }

    return false;
}

/// <summary>The scan has to be able to fail, or a clean folder proves nothing.</summary>
[Theory]
[InlineData("....caBX....")]
[InlineData("jumb")]
[InlineData("urn:c2pa:manifest")]
public void The_stamp_scan_catches_a_file_that_carries_one(string text) =>
    Assert.True(CarriesAStamp(System.Text.Encoding.ASCII.GetBytes(text)));

[Fact]
public void The_stamp_scan_passes_a_plain_png_header()
{
    byte[] header =
    [
        0x89, (byte)'P', (byte)'N', (byte)'G', 0x0D, 0x0A, 0x1A, 0x0A,
        0, 0, 0, 13, (byte)'I', (byte)'H', (byte)'D', (byte)'R',
    ];

    Assert.False(CarriesAStamp(header));
}
```
*Live from [`api/TheYard.Tests/DiagramPageTests.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Tests/DiagramPageTests.cs#L200-L304), region `as-drawn`, read from this build at 1d6b50b.*

The photographs that arrived the same way carry the same credential, in the
image's own metadata rather than in markup: at this writing, eighteen of the
screenshots under `docs/images`. A credential in a photograph changes nothing
on the page and says nothing false about how the file travelled, so those
stay as they are unless the author wants them gone; a re-copy through the
repository's own shell removes it.

## Where it sits

This one sits outside the rings: it is a process decision about serving the records from the site and gating every ship on the test suites, so the onion and SOLID do not apply to it.

## Files

Documentation:

- [`src/library/documents.ts`](https://github.com/SteveStout/TheYard/blob/main/src/library/documents.ts): every document the sidebar can
  open, its title, its menu and its kind, one record.
- [`api/TheYard.Api/DocumentationCatalog.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/DocumentationCatalog.cs): the same documents by slug on
  the server, held to the sidebar's list by a test (ADR: The staff review).
- [`docs/best-practices/BEST-PRACTICES.md`](https://github.com/SteveStout/TheYard/blob/main/docs/best-practices/BEST-PRACTICES.md), [`docs/hosting/HOSTING.md`](https://github.com/SteveStout/TheYard/blob/main/docs/hosting/HOSTING.md),
  [`docs/ci-cd/CICD.md`](https://github.com/SteveStout/TheYard/blob/main/docs/ci-cd/CICD.md): the three overviews the records hang under.

Testing:

- [`.github/workflows/ci.yml`](https://github.com/SteveStout/TheYard/blob/main/.github/workflows/ci.yml): the three suites as CI jobs, shown
  live below; a red job stops the deploy (ADR: The deploy pipeline).
- [`api/TheYard.Tests/TheYard.Tests.csproj`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Tests/TheYard.Tests.csproj) and the tests beside
  it: the API suite, integration tests over the real host.
- [`vite.config.ts`](https://github.com/SteveStout/TheYard/blob/main/vite.config.ts): the unit suite's configuration (vitest, only
  `src/**/*.test.ts`).
- [`playwright.config.ts`](https://github.com/SteveStout/TheYard/blob/main/playwright.config.ts): the end-to-end suite, which launches the
  API and the dev server itself and drives the installed Chrome.
- [`tests/e2e/smoke.spec.ts`](https://github.com/SteveStout/TheYard/blob/main/tests/e2e/smoke.spec.ts) and the specs beside it: the browser
  checks, one file per surface.

```yaml
# Five jobs on a pull request: style first, then one per suite and the SQL
# project behind it. The suites are separate so a failing
# browser test does not hide a failing unit test, and so the slowest one (the
# end-to-end job, which installs Chrome) does not gate the other two. The
# frontend job also typechecks and builds: a type error should fail before a
# test does.
jobs:
  # #region style-job
  # Format drift fails the build rather than getting argued about in review.
  # It runs first and on its own because it is the cheapest job here: a style
  # failure should not cost the time of a browser suite to discover.
  #
  # dotnet format checks three things at once against .editorconfig: whitespace,
  # using order, and unused usings. Prettier owns the TypeScript and the CSS.
  # oxlint owns the rules a formatter cannot see. The typecheck in the frontend
  # job already enforces noUnusedLocals and noUnusedParameters, so the linter is
  # not asked to repeat it (ADR-028).
  style:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - uses: actions/setup-dotnet@v6
        with:
          dotnet-version: 10.0.x
      - run: npm ci
      - run: npm run format:check
      - run: npm run lint
      - run: npm run format:api:check
  # #endregion style-job

  frontend:
    # The style job's comment says it runs first so a missing semicolon does not
    # cost a browser suite to discover. That was a description of an intention
    # until this line existed: without `needs` these all start at once and the
    # claim was decoration (the staff review, 2026-09-03).
    needs: style
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npx tsc -b
      - run: npm test
      - run: npm run build

  api:
    needs: style
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-dotnet@v6
        with:
          dotnet-version: 10.0.x
      # #region coverage
      # The suites are counted as well as run.
      #
      # A repository can say "276 tests" and mean very little by it. What a
      # reader wants to know is what they touch, and coverlet is already a
      # dependency of the test project, so the number costs one flag.
      #
      # The floor is a ratchet rather than a target. It sits just under where
      # the suites actually are, so a change that quietly stops testing
      # something fails here, and it is not set so high that somebody is
      # tempted to write a test that executes a line without asserting on it.
      # A number chased for its own sake buys nothing.
      # Output kept, and named on a failure, for the same reason the browser
      # suite does it: a red job whose only public word is "exit code 1" costs
      # somebody a session, and the full log needs a sign-in this repository
      # cannot assume its reader has. This job went red once on a commit that
      # changed nothing but documentation, and there was no way to say which
      # test without one.
      # The transcript goes to the runner's temp directory, not into the
      # checkout. A test in this suite reads every text file in the repository
      # and holds it to the house rule about em dashes; the console logger this
      # job asks for prints each test's name with its parameters, one of which
      # is the em dash that rule forbids, so a transcript written beside the
      # source made the suite fail on itself, on the runs where the line was
      # flushed before the scan reached it and not on the others
      # (ADR: Where a gate lives, addendum).
      # Everything except the tests that need the real Cosmos DB account. This
      # runner has no Azure credential and is not getting one, so those run on
      # the ship gate on Steve's machine as the signed-in principal; here they
      # are filtered out by their trait rather than skipped, because a skipped
      # test is a green check that checked nothing (ADR: A second store on
      # Cosmos DB, and what it costs).
      # And the coverage floor measures what this runner runs. The document
      # store's adapters are exercised by those filtered tests and by the whole
      # suite booted on the real account on the ship gate, none of which can
      # happen here, so that one assembly is left out of the number this job
      # holds to its floor. The first run with it counted went red at 83.2 per
      # cent of lines for code that was tested an hour earlier on a machine
      # with a credential. Lowering the floor would have hidden the same gap in
      # every other project; naming the assembly hides only the one this runner
      # cannot reach, with the reason beside it.
      # And code a source generator writes is left out by its GeneratedCode
      # attribute: the regular expressions and the OpenAPI comment readers are
      # written by the compiler at build time, nobody here writes or reviews
      # them, and they were a third of every branch counted. Counted, they held
      # the number down; dropped, the floor measures the code people write
      # (ADR: Counting what the tests cover, the addendum on generated code).
      - name: Run the .NET suites
        run: |
          set -o pipefail
          dotnet test api --collect:"XPlat Code Coverage" --results-directory TestResults \
            --filter "Store!=cosmos" \
            --logger 'console;verbosity=normal' \
            -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Exclude="[TheYard.Infrastructure.Cosmos]*" \
               DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.ExcludeByAttribute="GeneratedCodeAttribute" \
            2>&1 | tee "$RUNNER_TEMP/dotnet-output.txt"
      - name: Say which test failed, on the run page
        if: failure()
        run: |
          if [ ! -f "$RUNNER_TEMP/dotnet-output.txt" ]; then
            echo "::error::the .NET suites left no output to report"
            exit 0
          fi
          # Three lines after each marker, because the marker is the label and
          # the detail is underneath it: "Assert.Contains() Failure" says which
          # assertion and the line below it says what was actually in the
          # collection, and that second line is the one somebody needs.
          grep -E -A 3 '^[[:space:]]*(Failed |Error Message:)' "$RUNNER_TEMP/dotnet-output.txt" \
            | grep -v '^--$' \
            | head -n 24 \
            | while IFS= read -r line; do
                printf '::error::%s\n' "$(printf '%s' "$line" | tr -d '\r' | cut -c1-400)"
              done
          # dotnet writes its totals two different ways depending on the
          # logger: one 'Passed! - Failed: 0, Passed: 281 ...' line, or a
          # four-line 'Test Run Failed. / Total tests: ...' block. The
          # console logger this job asks for produces the second, so a
          # pattern that only knew the first printed nothing at all.
          grep -E '^[[:space:]]*(Passed!|Failed!|Test Run (Failed|Succeeded)\.|Total tests:|(Passed|Failed|Skipped):[[:space:]]+[0-9]+)' "$RUNNER_TEMP/dotnet-output.txt" \
            | head -n 6 | while IFS= read -r line; do
            printf '::error::%s\n' "$(printf '%s' "$line" | tr -d '\r' | cut -c1-400)"
          done
          {
            echo '## The .NET suites failed'
            echo
            echo '```'
            grep -E -A 6 '^[[:space:]]*Failed ' "$RUNNER_TEMP/dotnet-output.txt" | head -n 80
            echo '```'
          } >> "$GITHUB_STEP_SUMMARY"
      - name: Count what the tests cover
        if: always()
        run: python3 .github/coverage.py TestResults 90.0 75.0

  # #region database-job
  # The database is compiled like everything else. api/TheYard.Database is the
  # authority for the SQL Server schema (ADR: Data first, and the database in
  # source control), and Microsoft.Build.Sql turns its .sql files into a DACPAC,
  # so a column with a type that does not exist or a foreign key pointing at
  # nothing fails here rather than during a publish.
  #
  # It is a job of its own and not a step in the api job, because `dotnet build`
  # on the solution does not build a SQL project. That was found the hard way:
  # a solution build reported success, the DACPAC on disk was the previous one,
  # and a schema change was published that did not contain the change.
  database:
    needs: style
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-dotnet@v6
        with:
          dotnet-version: 10.0.x
      - run: dotnet build api/TheYard.Database/TheYard.Database.sqlproj -c Debug
      - name: The build has to have produced a package
        run: test -f api/TheYard.Database/bin/Debug/TheYard.Database.dacpac
  # #endregion database-job

  e2e:
    needs: style
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - uses: actions/setup-dotnet@v6
        with:
          dotnet-version: 10.0.x
      - run: npm ci
      - run: npx playwright install --with-deps chrome
      # #region e2e-evidence
      # Keep the output, and on a failure put it on the run page.
      #
      # The report artifact below is the full evidence, and downloading it needs
      # a GitHub login. The run page does not. A red build whose only public
      # words are "Process completed with exit code 1" costs somebody a session
      # to find out what broke, which is the same problem the html reporter was
      # added to solve, one step further out.
      #
      # pipefail so that tee does not swallow the exit code and turn a failing
      # suite into a passing step.
      - name: Run the browser suite
        run: |
          set -o pipefail
          npx playwright test 2>&1 | tee "$RUNNER_TEMP/playwright-output.txt"
      - name: Say what failed, on the run page
        if: failure()
        run: |
          # Two places, because they are readable by different people.
          #
          # The step summary is the nice one and it renders only for somebody
          # signed in. Annotations are the ones that survive: they come back
          # from the public API and show at the top of the run page, which is
          # what a person with no login to this repository can actually read.
          # The first attempt at this wrote only a summary and taught us that.
          if [ ! -f "$RUNNER_TEMP/playwright-output.txt" ]; then
            echo "::error::the browser suite left no output to report"
            exit 0
          fi
          # '[Tt]imeout of' rather than 'Timeout of': a test that runs out
          # of time says 'Test timeout of 90000ms exceeded.', which the
          # capital-only spelling missed, and that line is the whole
          # finding when the failure is a hang.
          grep -E '^[[:space:]]+[0-9]+\) |Error: |[Tt]imeout of |Timeout: |Locator: |Expected: |Received' "$RUNNER_TEMP/playwright-output.txt" \
            | head -n 20 \
            | while IFS= read -r line; do
                # One annotation per line, trimmed: a control character or a
                # newline inside one silently truncates the whole annotation.
                printf '::error::%s\n' "$(printf '%s' "$line" | tr -d '\r' | cut -c1-400)"
              done
          tail -n 4 "$RUNNER_TEMP/playwright-output.txt" | while IFS= read -r line; do
            printf '::error::%s\n' "$(printf '%s' "$line" | tr -d '\r' | cut -c1-400)"
          done
          {
            echo '## The browser suite failed'
            echo
            echo '```'
            tail -n 150 "$RUNNER_TEMP/playwright-output.txt"
            echo '```'
          } >> "$GITHUB_STEP_SUMMARY"
      # #endregion e2e-evidence
      - uses: actions/upload-artifact@v7
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/
```
*Live from [`.github/workflows/ci.yml`](https://github.com/SteveStout/TheYard/blob/1d6b50b/.github/workflows/ci.yml#L46-L292), region `ci-jobs`, read from this build at 1d6b50b.*

## Addendum, 2026-09-02: the diagram, the screenshots, and what counts as commented

The evening of the second build day, Steve asked for a diagram of the
infrastructure, as many screenshots and code references as possible, and
ruled that code shown in a record counts as commented. Three rules follow
from it, all in force.

- **The infrastructure has a picture.** [`docs/images/infrastructure.svg`](https://github.com/SteveStout/TheYard/blob/main/docs/images/infrastructure.svg)
  is drawn by hand from the records and the pipeline logs, rendered to
  [`docs/images/infrastructure.png`](https://github.com/SteveStout/TheYard/blob/main/docs/images/infrastructure.png), and served at the top of the
  Hosting page and the README. It is redrawn when a box changes; a picture
  that disagrees with the records is a bug in the picture.
- **Every surface has a screenshot from the live site.** The records that
  decided a look carry a capture of that look from the domain, taken by the
  same headless Chrome the end-to-end suite uses, signed out, so the picture
  is what a visitor sees and not what a developer sees. The captures live in
  [`docs/images`](https://github.com/SteveStout/TheYard/tree/main/docs/images) beside the configuration screenshots from the first
  day. The first pass found a real defect: a long Azure event message ran
  past the edge of its card, fixed in the same version.
- **Code shown in a record is documented by the record, and teaches.** A
  region marker names its record in a comment, the record explains the code
  beside the live sample, and the region itself opens with a comment written
  for someone meeting the pattern for the first time: why it is done this
  way, and what breaks otherwise. Steve's rule for it, on the second build
  day: "any code samples must be well documented, that helps people learn
  and grow." Code no record shows keeps the comments a reader needs at the
  line. Code that no record shows keeps its own comments. So the
  question for a reviewer is never "is this commented" but "which record
  shows this", and the Files section at the end of every record answers it.
- **A diagram opens on its own page.** The picture in a record is a preview
  that links to the drawing on a page of its own, opened in a new tab,
  zoomable, with its text selectable; the Data Flow page's text diagram
  became a drawing in the same style as the infrastructure one. Recorded in
  ADR: Diagram pages.

The unit suite's configuration and the end-to-end suite's two servers, read
from this build ([`vite.config.ts`](https://github.com/SteveStout/TheYard/blob/main/vite.config.ts), [`playwright.config.ts`](https://github.com/SteveStout/TheYard/blob/main/playwright.config.ts)):

```ts
// Vitest reads its settings from the same file as the dev server, which is
// why there is no vitest.config.ts. The include pattern keeps it to the unit
// tests, and the one CSS entry exists because Vitest blanks CSS imports it
// is not told to process, which would leave the palette test with nothing to
// measure.
test: {
  // Unit tests only; tests/e2e belongs to Playwright. render/ holds the rendering service's own.
  include: ['src/**/*.test.ts', 'render/**/*.test.ts'],
  // allowOnly is deliberately not set. Vitest already defaults it to
  // !process.env.CI, which is the rule this project wants: a committed
  // `it.only` turns a suite into one test and still reports green, so CI
  // refuses one while a developer debugging a single test can focus it.
  // Writing the default out would need @types/node in a project that has
  // never needed it, for one boolean. What is checked instead is that
  // nothing here turns it off (ADR: Broken windows, and the rule that
  // answers them).
  // Vitest blanks CSS imports it is not told to process. These are read as
  // text: the four token sheets (through src/lib/styleSheet.ts, for the
  // contrast tests and the swatches), the panels sheet (panels.test.ts), the
  // ribbon sheet (ribbons.test.ts) and every component sheet (icons.test.ts,
  // for a stroke written as a number of its own).
  css: {
    include: [
      /(colors|sizes|typography|effects)\.css\?raw$/,
      /panels\.css\?raw$/,
      /Ribbons\.module\.css\?raw$/,
      /(components|library)\/.+\.module\.css\?raw$/,
    ],
  },
},
```
*Live from [`vite.config.ts`](https://github.com/SteveStout/TheYard/blob/1d6b50b/vite.config.ts#L170-L199), region `unit-tests`, read from this build at 1d6b50b.*

```ts
// Playwright starts both servers itself, so `npm run test:e2e` needs nothing
// running first, and reuses ones already up, so it also works mid-development.
// Each server is considered ready when a real URL answers, not after a sleep.
webServer: [
  {
    // The ship gate has just built the API, so it says so and the server
    // starts without building it again, ten seconds twice per gate
    // (ADR: The five-minute gate). Everywhere else the build is wanted.
    command: process.env.YARD_API_PREBUILT
      ? 'dotnet run --no-build --project api/TheYard.Api'
      : 'npm run api',
    // The simulated room waits twenty seconds before answering a bid
    // (ADR-027). Zero here so the outbid test watches a lead change hands
    // instead of watching a clock. Nothing else sets this.
    // And a key for the visitor rows on the Admin tab, so the browser
    // suite can prove the table exists behind it and nowhere else
    // (ADR: Site activity, and the line an address does not cross).
    env: {
      Market__GraceSeconds: '0',
      Admin__Key: 'e2e-admin-key',
      Admin__VisitorRows: 'true',
      Logs__DrainSeconds: '2',
    },
    url: 'http://localhost:5210/api/facets',
    reuseExistingServer: true,
    timeout: 120_000,
  },
  {
    // The ship gate has just run vite build, so it serves that bundle with
    // vite preview: one script and one sheet per page instead of Vite
    // compiling and serving the module graph file by file to every page the
    // suite opens (ADR: The five-minute gate). Everywhere else, the dev server.
    command: process.env.YARD_WEB_PREVIEW
      ? 'npx vite preview --port 5173 --strictPort'
      : 'npm run dev',
    url: 'http://localhost:5173',
    reuseExistingServer: true,
    timeout: 120_000,
  },
  // The rendering service (ADR: A rendering service beside the API), when its build is made
  // (npm run build:render) and asked for: in front of the built site above, which stands in for
  // the API it reads and for the edge, so a page it draws is taken over by the bundle that site
  // serves. drawn.spec.ts opens it on 5220 and skips those pages when it is not running.
  ...(process.env.YARD_RENDER
    ? [
        {
          command: 'node render/server.mjs',
          env: {
            PORT: '5220',
            YARD_SITES: 'sql=http://localhost:5173',
            YARD_PASS_ORIGIN: 'http://localhost:5173',
            // The suite's machine runs four browsers, the API and the site at once, and a list read
            // there can pass the live deadline; the pages here are drawn with every read in.
            YARD_DEADLINE_MS: '20000',
          },
          url: 'http://localhost:5220/readyz',
          reuseExistingServer: true,
          timeout: 120_000,
        },
      ]
    : []),
],
```
*Live from [`playwright.config.ts`](https://github.com/SteveStout/TheYard/blob/1d6b50b/playwright.config.ts#L70-L131), region `web-servers`, read from this build at 1d6b50b.*

The one record of every document the sidebar can open
([`src/library/documents.ts`](https://github.com/SteveStout/TheYard/blob/main/src/library/documents.ts), reading [`src/library/records.ts`](https://github.com/SteveStout/TheYard/blob/main/src/library/records.ts) and [`src/library/pages.ts`](https://github.com/SteveStout/TheYard/blob/main/src/library/pages.ts)):

```ts
// One record, one place. The URL's last segment is the slug the API looks up
// in DocumentationCatalog.cs, and a test fails if the two lists ever disagree, so a
// document can never appear in the menu without being servable, or the other
// way around (ADR-017). The documents themselves are in records.ts and
// pages.ts; this is the one list both are read through.
export const DOCS: Record<DocKey, DocEntry> = { ...RECORDS, ...PAGES };
```
*Live from [`src/library/documents.ts`](https://github.com/SteveStout/TheYard/blob/1d6b50b/src/library/documents.ts#L37-L42), region `docs-record`, read from this build at 1d6b50b.*

## Addendum, 2026-09-09: the picture redrawn for two stores

The infrastructure drawing gained a row: the Azure Cosmos DB store beside the
Azure SQL one, and a box for both stores in one process with the toggle, the
peer read and the proof (ADR: One container, both stores; ADR: Same
performance, proven). The ship lane's arrows up to the origin now route
between the two store boxes, the counts in the CI box were brought up to the
suites as they run today, and the date in the heading moved. The preview is
rendered from the SVG at twice its size by
[`docs/images/render.mjs`](https://github.com/SteveStout/TheYard/blob/main/docs/images/render.mjs),
in the repository's own Chrome with the site's font, so the picture on the
README is the drawing and not a second drawing of it.

## Addendum, 2026-09-13: the photographs stripped too, and the test widened to them

The first addendum left the eighteen PNG and JPEG screenshots that had
arrived by the same route with their credential in place, on the argument
that a credential in a photograph changes nothing on the page. The owner's
answer, on 9 September, was that a signed machine-provenance credential on
his images is exactly the signal he asked to have removed, and the argument
does not survive that: the rule is about what the repository carries, not
about what a reader can see.

Measured before the strip, every raster image under `docs/images` read for
the marks: 64 images, 18 carrying `c2pa`, `jumb`, `jumd` and `urn:c2pa`, the
four `cosmos-*.png` from 8 September and fourteen captures from 9 September
(the `netlify-*.jpg`, `timing-*.png`, `toggle-site-*.png` and
`wix-dns-two-sites.jpg` files). In a PNG the manifest is a JUMBF box inside a
`caBX` chunk; in a JPEG it is a JUMBF box carried across `APP11` segments.
Both are ancillary: the pixels are in the `IDAT` chunks and the entropy-coded
scan, and neither is touched by removing the box.

The strip ran through the repository's own shell rather than through the
tool that stamps, one script over all eighteen: the `caBX` chunks dropped
from the PNGs, the `APP11` segments dropped from the JPEGs, and every file
verified afterwards on two counts, that no mark remains and that the image
data is byte for byte what it was, the `IDAT` chunks concatenated for a PNG
and everything outside the removed segments for a JPEG. A copy as drawn does
not exist for a screenshot the way it does for a drawing, so identical image
data is the proof that stands in for the checksum.

`DiagramPageTests` now reads every PNG and JPEG under `docs/images` for the
same marks the SVG check reads for, and the scan is proven able to fail on
a file that carries one. The test, read from this build:

```csharp
/// <summary>
/// A drawing's source is what its author drew and nothing else (ADR-006,
/// the addendum on the provenance stamp). One of the tools that carries a
/// file from the assistant's workspace to the developer's machine writes a
/// signed content credential into every image it touches: a base64
/// manifest in a metadata element and a namespace on the root. In a
/// photograph that is a few kilobytes nobody sees; in an SVG it was half
/// the file, and the diagram page inlines the whole file into its HTML on
/// every visit. Every SVG the repository keeps is read for it here, so the
/// next stamped copy is caught by the gate and not by a file size that
/// looked wrong.
/// </summary>
[Fact]
public void Every_drawing_is_the_source_as_drawn_with_no_stamp_written_into_it()
{
    string images = Path.Combine(Repo.Root(), "docs", "images");
    var drawings = Directory.EnumerateFiles(images, "*.svg").OrderBy(path => path).ToList();
    Assert.NotEmpty(drawings);

    var stamped = drawings
        .Where(path =>
        {
            string source = File.ReadAllText(path);
            return source.Contains("<metadata", StringComparison.Ordinal)
                || source.Contains("c2pa", StringComparison.OrdinalIgnoreCase);
        })
        .Select(Path.GetFileName)
        .ToList();

    Assert.True(
        stamped.Count == 0,
        $"stamped on the way in, strip and re-copy: {string.Join(", ", stamped)}");
}

/// <summary>
/// The same rule for the photographs (ADR-006, the second addendum on the
/// provenance stamp). The SVG check above shipped on 9 September and the
/// eighteen PNG and JPEG screenshots that had arrived by the same route
/// were noted and left, on the argument that a credential in a photograph
/// changes nothing on the page. The owner's answer was that a signed
/// machine-provenance credential on his images is exactly the signal he
/// asked to have removed, so they were stripped on 13 September and this
/// keeps them that way. In a PNG the manifest is a JUMBF box in a caBX
/// chunk; in a JPEG it is a JUMBF box across APP11 segments. Both carry the
/// same marks, so one scan reads every raster image for them.
/// </summary>
[Fact]
public void Every_photograph_is_the_capture_as_taken_with_no_stamp_written_into_it()
{
    string images = Path.Combine(Repo.Root(), "docs", "images");
    var photographs = Directory.EnumerateFiles(images)
        .Where(path => Path.GetExtension(path).ToLowerInvariant() is ".png" or ".jpg" or ".jpeg")
        .OrderBy(path => path)
        .ToList();
    Assert.NotEmpty(photographs);

    var stamped = photographs
        .Where(path => CarriesAStamp(File.ReadAllBytes(path)))
        .Select(Path.GetFileName)
        .ToList();

    Assert.True(
        stamped.Count == 0,
        $"stamped on the way in, strip through the repository's own shell: {string.Join(", ", stamped)}");
}

/// <summary>
/// The marks a content credential leaves in a raster file: the JUMBF box
/// types, the C2PA manifest label and the PNG chunk that carries them.
/// Bytes rather than text, because a JPEG is not text and a credential's
/// label is ASCII either way.
/// </summary>
private static bool CarriesAStamp(byte[] bytes)
{
    ReadOnlySpan<byte> span = bytes;
    foreach (string mark in new[] { "c2pa", "jumb", "jumd", "caBX", "urn:c2pa" })
    {
        if (span.IndexOf(System.Text.Encoding.ASCII.GetBytes(mark)) >= 0)
        {
            return true;
        }
    }

    return false;
}

/// <summary>The scan has to be able to fail, or a clean folder proves nothing.</summary>
[Theory]
[InlineData("....caBX....")]
[InlineData("jumb")]
[InlineData("urn:c2pa:manifest")]
public void The_stamp_scan_catches_a_file_that_carries_one(string text) =>
    Assert.True(CarriesAStamp(System.Text.Encoding.ASCII.GetBytes(text)));

[Fact]
public void The_stamp_scan_passes_a_plain_png_header()
{
    byte[] header =
    [
        0x89, (byte)'P', (byte)'N', (byte)'G', 0x0D, 0x0A, 0x1A, 0x0A,
        0, 0, 0, 13, (byte)'I', (byte)'H', (byte)'D', (byte)'R',
    ];

    Assert.False(CarriesAStamp(header));
}
```
*Live from [`api/TheYard.Tests/DiagramPageTests.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Tests/DiagramPageTests.cs#L200-L304), region `as-drawn`, read from this build at 1d6b50b.*

The rule, stated once for every image type: a file that crossed from the
assistant's workspace is read back before it is committed, and the copy that
goes in is the one the repository's own shell wrote.

