# Built with AI

TheYard was built by one engineer using AI as a force multiplier. This page says what that meant in
practice: what the AI wrote and what I decided. It also covers what governed the result and what it cost,
and then what went wrong. Everything here links to the record or the file that proves it.

## In plain words

This page says how the site was built: one engineer used AI to write most first drafts and kept the choices a tool cannot make. It also lists what the work cost and what went wrong, and links each item to the record or file that proves it.

What that is worth: a developer sees which parts a person decided and which tests held the AI's output, and the organization gets a way of using AI where every claim points at its proof.

## What the AI wrote

The first draft of most of the code and most of the decision records. The deployment scripts and the tests
too. Commits carry the co-author trailer and the repository carries CLAUDE.md, the standing instructions the AI
worked under. None of that is hidden and no history has been rewritten.

## What I decided

The choices a tool cannot make, each with its record.

- The domain and the fresh repository, dropping a take-home's lineage for a platform I own.
- The hosting path under a free-trial subscription: Container Instances behind an edge for TLS for the
  first three weeks, then both sites on one App Service plan once the memory and the bill were on the same
  page and the quota wall turned out to be a region, with Front Door still designed and still refused
  (HOSTING.md, ADR-004, ADR-007, ADR-079).
- A second store on Cosmos DB beside Azure SQL, the same image on both, and the bar it had to clear: as
  fast as SQL Server on a different data structure at the lowest cost (ADR-058, ADR-059).
- One container running both stores with a store toggle at the top of every page, then a permanent address
  for the second site (ADR-066, ADR-069).
- The sidebar over dropdowns and the palette, then the rule that every diagram opens on its own page
  (ADR-013, ADR-016, ADR-020).
- The one gate: every check once per version, on both stores, before anything rolls (ADR-068 and its addendum).
- Every test, specified before the code it holds was written (test driven development).
- Every GO. Nothing rolled to Azure without a written go from me.

## What governed it

**The test gate.** Every version runs all three suites once, in the ship's gate: 897 xUnit tests, 458 Vitest tests at 1.0.3.161 and 160 Playwright tests. The ship
gate runs the API suite against both stores and was measured on 9 September at 275 seconds green on a quiet machine, 302
to 342 seconds with the developer's browser open, and 372 seconds cold after a restart (ADR-068); the gate on 1.0.0.182 took 493 seconds, and the gate on 1.0.3.59, with both store passes carried forward from 1.0.3.58, took 641. A push that fails the gate does not roll.

**The records.** One hundred decision records, each carrying the decision, the trade-off and the number behind
it, with a Files section pointing at the code it governs. Code shown in a record is read from the running build, so a
record cannot drift from the code it describes.

**The reviews.** A self review before one ship found eleven defects, four of them serious: a bid acceptable
below the going rate, a lost update on concurrent bids, a poll erasing a just-placed bid, and the simulated
room hammering three cars (ADR-027). Later, three readers with no memory of the project, an interviewer, a
junior developer and an architect, reviewed the checkout cold, and every finding got a decision: fixed with
its version, designed with the version it ships in, or accepted with its reason (ADR-070).

**The measurements.** On 3 of 8 paths the two stores answer in the same time. On the other 5 the difference
is the round trip to the store, 39 ms to Azure SQL Database against 2 ms to Azure Cosmos DB, and taking one
round trip per operation off each side leaves them the same (ADR-067). A sign-in costs 2.00 request units, a
bid write 6.52, a raise 11.29, a registration 13.04 (ADR-064, ADR-059). Leaving the indexing policy at the
default cost 16.07 request units a document on the bulk seed against 8.84 tuned, 21 minutes against 13, and
8,407 documents refused by throttling that the tuned seed never saw (ADR-065).

## What it cost

- The Cosmos DB account on the free tier: $0.00 a month, 1000 RU/s shared, with local auth disabled so no
  key exists (ADR-058, ADR-059).
- The compute: two container groups at $34.44 a month each at list price until 20 September, one
  App Service plan at $12.41 a month for both sites from then, and the same plan at $24.82 since
  8 October, when the rendering service joined the two sites on it, drawn from trial credit either way
  (ADR-059, ADR-079, ADR-096).
- The edge: Netlify's free plan allots 300 credits a month, and 11 production deploys had consumed 165 of
  them at 15 each while serving the site cost almost nothing. Application pushes no longer redeploy the
  edge, so they cost zero credits (ADR-007).

## What went wrong

- **1.0.0.90 took the live site to files.** A transitive package bump broke managed-identity token
  acquisition for SQL inside the container. The Azure SQL container came up on files twice, once on the roll
  and once on a restart, against a database that was demonstrably online, with a `SqlException: A task was
  canceled` wrapping a `TaskCanceledException` in the Azure retry pipeline both times. Rolled back by hand,
  the package pinned with the reason in the project file, and the lesson recorded: pin shared Azure packages
  across projects (ADR-059 addendum).
- **Warming both catalogues before serving turned a two-minute suite into a thirty-minute crawl** that
  looked like a hang. Only the default store warms before serving now (ADR-066).
- **A per-commit changelog line once invented a version that never shipped**, because two commits pushed
  together are one deploy. The version is read from the changelog's top line now (ADR-031).

## How to check any of this yourself

- Open the Admin tab on either site: live health checks per store, the paired-round comparison card, the
  last container events, and the log of every store operation.
- Open any record: the Files section links the code it governs, and the live blocks are read from the
  container's own source at request time.
- Run the gate yourself, or read the CI runs linked from CICD.md.
- Read the Performance page beside this one for what the measurements add up to on the smallest machine that will hold it.

## Files

- [`CLAUDE.md`](https://github.com/SteveStout/TheYard/blob/main/CLAUDE.md): the standing instructions the AI worked under, kept in the repository rather than hidden.
- [`.github/workflows/ci.yml`](https://github.com/SteveStout/TheYard/blob/main/.github/workflows/ci.yml): the three suites on a pull request, and the coverage annotation. The jobs, read from this build:

```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.*

- [`api/TheYard.Api/DocumentationCatalog.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/DocumentationCatalog.cs): every document this page sits beside, served from the checkout by slug.
- [`api/TheYard.Tests/PublicFaceTests.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Tests/PublicFaceTests.cs): the test that holds the record count in every living document, this page included, and the README's test counts to what the suites declare.
- [`docs/decisions/ADR-068-the-five-minute-gate.md`](https://github.com/SteveStout/TheYard/blob/main/docs/decisions/ADR-068-the-five-minute-gate.md), [`docs/decisions/ADR-027-competing-bidders.md`](https://github.com/SteveStout/TheYard/blob/main/docs/decisions/ADR-027-competing-bidders.md), [`docs/decisions/ADR-070-three-readers-with-no-memory.md`](https://github.com/SteveStout/TheYard/blob/main/docs/decisions/ADR-070-three-readers-with-no-memory.md), [`docs/decisions/ADR-067-same-performance-proven.md`](https://github.com/SteveStout/TheYard/blob/main/docs/decisions/ADR-067-same-performance-proven.md), [`docs/decisions/ADR-059-a-second-store-priced.md`](https://github.com/SteveStout/TheYard/blob/main/docs/decisions/ADR-059-a-second-store-priced.md): the records the numbers above come from.
