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: 1797 xUnit tests, 589 Vitest tests at 1.0.4.132 and 196 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 and twenty-three 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: the standing instructions the AI worked under, kept in the repository rather than hidden.
.github/workflows/ci.yml: the three suites on a pull request, and the coverage annotation. The jobs, read from this build:
jobs:
style:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- uses: ./.github/actions/dotnet
- run: npm ci
- run: npm run format:check
- run: npm run lint
- run: npm run format:api:check
frontend:
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 run stack:check
- run: npm test
- run: npm run build
api:
needs: style
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/dotnet
- 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]*,[TestProject]*" \
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
- name: The quality ratchet
if: always()
run: python3 .github/quality.py . --ratchet .github/quality-ratchet.json
functions:
needs: style
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/dotnet
- name: Run the job hunt Function's tests
run: dotnet test functions/jobhunt.Tests --logger 'console;verbosity=normal'
database:
needs: style
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/dotnet
- 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
- run: dotnet build functions/JobHunt.Database/JobHunt.Database.sqlproj -c Debug
- name: The campaign's build has to have produced a package
run: test -f functions/JobHunt.Database/bin/Debug/JobHunt.Database.dacpac
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: ./.github/actions/dotnet
- run: npm ci
- run: npx playwright install --with-deps chrome
- 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"
- uses: actions/upload-artifact@v7
if: failure()
with:
name: playwright-report
path: playwright-report/
Live from .github/workflows/ci.yml, region ci-jobs, read from this build at 9094953.