Skip to content

ADR: The editor is set up by the repository

ADR: The editor is set up by the repository

Status: accepted, 2026-10-13 (1.0.3.121). TheYard and The Shed each carry a .vscode folder: the recommended extensions, the workspace settings, every command as a task, and F5. A page under App Architecture, Working in VS Code, walks a new developer through it with screenshots, and a test holds the configuration to the code it names.

In plain words

Until this record, a developer who cloned this repository got the code and the README and had to set the editor up alone: find the solution file, work out the ports, write a launch configuration, learn which npm script is which. Now the repository carries that setup. Open the folder, accept the extensions it offers, press F5, and the API is under the debugger with the page in Chrome beside it. The Shed gets the same, in its own folder, for the demo shape.

What that is worth: a new developer makes a change and watches it under the debugger inside the first half hour, with nobody walking them through it; and the organization can hand this repository to a new hire, a reviewer or a demo laptop and have it run the same way on each.

Context

Steve, 13 October: a session "that does nothing but make sure my Yard and Shed are very easy to work with from visual studio code and I have complete readmes and step by step with screen shots and verifies everything is easy for a jr dev or demo". A survey of the tree at 1.0.3.119 found: no .vscode folder in either project; a README that gave the commands and nothing about the editor; and a Start here page that said what to run and read but not how to debug or where the tests show up. The dry run that morning, a fresh clone on a four-core laptop, ran every README command green (npm ci 32 s, the solution built with warnings as errors in 47 s, 918 xUnit on SQLite in 63 s, Vitest in 19 s, The Shed's 145 and 21 in 24 s, both servers up and the proxy answering inside 20 s), so the commands were right. What was missing was the editor around them.

Two ways to carry an editor setup were weighed. A .code-workspace file with both projects as roots puts two solutions in one window, and C# Dev Kit opens one solution at a time, which leaves a new developer choosing between the two at the first prompt. Two .vscode folders, one per project, each opening its own solution, put the choice where it already is: which folder you opened. TheYard's folder also carries The Shed's tasks and its launch configuration, so The Shed runs from the Yard's window without opening it, and The Shed's own folder has the same for the demo.

Decision

Four files per project, nothing personal in any of them. extensions.json recommends the extensions the gate's checks assume (C#, C# Dev Kit, Prettier, Oxc, Vitest, Playwright Test, EditorConfig, and the Containers and Bicep extensions for what the site runs on). settings.json names the default solution, turns on format on save with the same Prettier and the same .editorconfig the gate checks against, pins the project's own TypeScript, and keeps the editor's watcher and search off bin/, obj/, node_modules/ and the build outputs, for the same reason vite.config.ts keeps Vite's watcher off api/: dotnet holds locks on obj/ while it builds. tasks.json holds every command the README gives, as a task with its command shown beside it in the picker, and one compound that runs the checks the gate runs in the order it runs them, without the browser. launch.json holds F5: the API under the .NET debugger on the port launchSettings.json names, the page in Chrome through Vite on the port the browser suite names, a compound of both, and The Shed with the browser opening on its listening line. Colour themes, keybindings and fonts stay in the user's profile, where they belong.

{
  // F5. The first configuration is the one a new developer wants: the API under the
  // debugger and the page in Chrome, with breakpoints on both sides
  // (ADR: The editor is set up by the repository).
  "version": "0.2.0",
  "configurations": [
    {
      // The .NET API under the debugger. Built first by the task below; the same
      // address and environment as api/TheYard.Api/Properties/launchSettings.json.
      "name": "Yard: API",
      "type": "coreclr",
      "request": "launch",
      "preLaunchTask": "Yard: build API",
      "program": "${workspaceFolder}/api/TheYard.Api/bin/Debug/net10.0/TheYard.Api.dll",
      "cwd": "${workspaceFolder}/api/TheYard.Api",
      "env": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "ASPNETCORE_URLS": "http://localhost:5210"
      },
      "console": "internalConsole",
      "stopAtEntry": false
    },
    {
      // The React page in Chrome with source maps, once Vite reports it is up.
      // Vite proxies /api to :5210, so this needs "Yard: API" running beside it,
      // which the compound below does.
      "name": "Yard: web in Chrome",
      "type": "chrome",
      "request": "launch",
      "preLaunchTask": "Yard: web only (Vite)",
      "url": "http://localhost:5173",
      "webRoot": "${workspaceFolder}"
    },
    {
      // The Shed from the same window: the API and its page are one process on :5120,
      // and the browser opens when it says it is listening. To point it at a folder of
      // your own, add FILES__HOME to env.
      "name": "Shed: run",
      "type": "coreclr",
      "request": "launch",
      "preLaunchTask": "Shed: build",
      "program": "${workspaceFolder}/samples/theshed/bin/Debug/net10.0/TestProject.dll",
      "cwd": "${workspaceFolder}/samples/theshed",
      "env": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "ASPNETCORE_URLS": "http://localhost:5120"
      },
      "console": "internalConsole",
      // It serves plain http on :5120, so it needs no certificate. Without this line the C#
      // extension sees the browser opening and, on a machine with no trusted HTTPS development
      // certificate, asks on every F5 whether to create and trust one.
      "checkForDevCert": false,
      "serverReadyAction": {
        "action": "openExternally",
        "pattern": "\\bNow listening on:\\s+(https?://\\S+)"
      }
    }
  ],
  "compounds": [
    {
      // F5 with this selected: both halves of TheYard, breakpoints on both sides.
      "name": "Yard: API + web (F5)",
      "configurations": ["Yard: API", "Yard: web in Chrome"],
      "stopAll": true
    }
  ]
}

Live from .vscode/launch.json, region *, read from this build at 565827b.

The configuration is held to the code. WorkspaceTests reads the three files with comments allowed and resolves every name: a task that runs npm run x fails when package.json has no x, a task that runs dotnet build p fails when p is not a project, a launch configuration fails when its DLL path does not match a project in the tree and that project's target framework and assembly name, when its port is not the one in that project's launchSettings.json, or when the task it runs first is not in tasks.json; the Chrome configuration fails when its port is not Vite's. The same test holds the page to the files: every task and every launch configuration is named on Working in VS Code, through the live sample of tasks.json the page carries and the prose around it, so a task added to the file shows on the page that explains it.

[Theory]
[InlineData(".vscode")]
[InlineData("samples/theshed/.vscode")]
public void Every_launch_configuration_runs_a_project_in_the_tree_on_the_port_its_own_settings_name(string folder)
{
    string root = Repo.Root();
    string home = Path.GetDirectoryName(Path.Combine(root, folder))!;
    using var launch = Read(root, folder, "launch.json");
    using var tasks = Read(root, folder, "tasks.json");
    var taskLabels = tasks.RootElement.GetProperty("tasks").EnumerateArray()
        .Select(task => task.GetProperty("label").GetString()!)
        .ToHashSet(StringComparer.Ordinal);
    var names = new List<string>();
    var wrong = new List<string>();

    foreach (var configuration in launch.RootElement.GetProperty("configurations").EnumerateArray())
    {
        string name = configuration.GetProperty("name").GetString()!;
        names.Add(name);
        if (configuration.TryGetProperty("preLaunchTask", out var pre) && !taskLabels.Contains(pre.GetString()!))
        {
            wrong.Add($"'{name}' runs the task '{pre.GetString()}' first, which {folder}/tasks.json does not define");
        }

        string type = configuration.GetProperty("type").GetString()!;
        if (type == "coreclr")
        {
            // The program is bin/Debug/<tfm>/<Assembly>.dll under the project folder;
            // the project file and its target framework are what the path must agree with.
            string program = configuration.GetProperty("program").GetString()!.Replace("${workspaceFolder}", home, StringComparison.Ordinal);
            var shape = Regex.Match(program.Replace('\\', '/'), @"^(?<project>.+)/bin/Debug/(?<tfm>net\d+\.\d+)/(?<assembly>[^/]+)\.dll$");
            Assert.True(shape.Success, $"'{name}' runs {program}, which is not a bin/Debug/<tfm>/<Assembly>.dll path");
            string projectFolder = shape.Groups["project"].Value;
            string? csproj = Directory.Exists(projectFolder)
                ? Directory.EnumerateFiles(projectFolder, "*.csproj").FirstOrDefault()
                : null;
            if (csproj is null)
            {
                wrong.Add($"'{name}' runs a DLL under {projectFolder}, which holds no project");
                continue;
            }
            string projectText = File.ReadAllText(csproj);
            string framework = Regex.Match(projectText, @"<TargetFramework>([^<]+)</TargetFramework>").Groups[1].Value;
            if (framework != shape.Groups["tfm"].Value)
            {
                wrong.Add($"'{name}' expects {shape.Groups["tfm"].Value} and {Path.GetFileName(csproj)} targets {framework}");
            }
            string assembly = Regex.Match(projectText, @"<AssemblyName>([^<]+)</AssemblyName>") is { Success: true } named
                ? named.Groups[1].Value
                : Path.GetFileNameWithoutExtension(csproj);
            if (assembly != shape.Groups["assembly"].Value)
            {
                wrong.Add($"'{name}' runs {shape.Groups["assembly"].Value}.dll and the project builds {assembly}.dll");
            }

            int configured = PortOf(configuration.GetProperty("env").GetProperty("ASPNETCORE_URLS").GetString()!);
            int settings = LaunchSettingsPort(root, Path.GetRelativePath(root, projectFolder));
            if (configured != settings)
            {
                wrong.Add($"'{name}' listens on {configured} and the project's launchSettings.json says {settings}");
            }
        }
        else if (type == "chrome")
        {
            // The page's address is Vite's, the one the browser suite and the proxy agree on.
            string config = File.ReadAllText(Path.Combine(root, "playwright.config.ts"));
            int vite = PortOf(Regex.Match(config, @"baseURL: '(http://localhost:\d+)'").Groups[1].Value);
            int configured = PortOf(configuration.GetProperty("url").GetString()!);
            if (configured != vite)
            {
                wrong.Add($"'{name}' opens port {configured} and Vite serves {vite}");
            }
        }
        else
        {
            wrong.Add($"'{name}' is of type '{type}', which this test does not know how to resolve");
        }
    }

    if (launch.RootElement.TryGetProperty("compounds", out var compounds))
    {
        foreach (var compound in compounds.EnumerateArray())
        {
            foreach (var member in compound.GetProperty("configurations").EnumerateArray())
            {
                if (!names.Contains(member.GetString()!))
                {
                    wrong.Add($"the compound '{compound.GetProperty("name").GetString()}' names '{member.GetString()}', which is not a configuration");
                }
            }
        }
    }

    Assert.True(names.Count >= 1, $"{folder}/launch.json holds no configuration");
    Assert.True(wrong.Count == 0, string.Join(Environment.NewLine, wrong));
}

Live from api/TheYard.Tests/WorkspaceTests.cs, region launch, read from this build at 565827b.

The page shows the real files. .vscode/ joined the roots a live block may read from, and the runtime image copies the folder beside the other sources the records read, so the extensions file, the launch file and the tasks file on the page are the ones in the build, not a paste. .dockerignore had excluded the folder as editor metadata; it no longer does, because it is documentation now.

What was left out, on purpose. No .code-workspace file (above). No devcontainer: the three installs are one winget line and the page says so. No per-project omnisharp.json or Dev Kit settings beyond the solution: the defaults are right for this tree. No task for the full gate: the gate is the ship's, runs on the machine that ships, and takes ten minutes; what a developer runs before a commit is the compound without the browser, about two minutes, and the browser suite as its own task.

Where it sits

Outside the rings: the editor's configuration, beside the code and read by nothing at run time. The one addition to the application is a root on the live-sample whitelist. Single responsibility: each of the four files answers one question (which extensions, which settings, which commands, what F5 does), and the test answers whether they still match the tree. It costs a folder most developers never open, and a test that reads JSON with comments; what would change it is a second editor in common use here, which would get its own folder and its own row in the test.

Files

Store

The Yard, home

Welcome to The Yard

Steven Stout’s showcase: a used-vehicle auction on .NET and React, built and run by one engineer, live on Azure, with its decisions written down.

Answering in

unread

0255075100

server time at p95, the usual five minutes of the last day.

Azure this month

unread

the bill, by resource, read from Azure. The Admin tab shows every line.

Since the last roll

unread

 

rolled blue and green, with no downtime.

People who have visited

unread

30 days agotoday

Scanners, my own tools' and my own reads left out.

  •    
  •    
  •    

Tech stack

Read from the build, never typed by hand

What TheYard runs on

What we use, where it runs, the version as built, and the file that proves it.

  • .NET 1014 projects
  • React 19.3TypeScript 7.0
  • Azure6 Bicep files
  • 9GitHub Actions workflows
LayerWhat we useWhere it runsVersionProof
Backendthe API
Backendthe API.NET and ASP.NET CoreApp Service, the API container10.0TheYard.Api.csproj
Entity Framework Core (SQL Server)App Service, talking to Azure SQL10.0.12TheYard.Infrastructure.csproj
Identity with JWT bearer authApp Service, the API container10.0.12TheYard.Api.csproj
OpenAPI with Scalar API docsApp Service, /api docs2.17.13TheYard.Api.csproj
Frontendthe page
Frontendthe pageReactYour browser19.3package.json
TypeScriptYour browser7.0package.json
ViteBuild step, in the container image8.3package.json
Server-side rendering on NodeApp Service, the renderer container24render/Dockerfile
Datatwo stores, one port
Datatwo stores, one portAzure SQL DatabaseAzure, managed SQLEF Core 10.0.12TheYard.Infrastructure.csproj
Azure Cosmos DBAzure, NoSQLSDK 3.63.2TheYard.Infrastructure.Cosmos.csproj
Azureinfrastructure as code
Azureinfrastructure as codeApp Service and Container Registry (Linux container)Azure App Service and ACRlive resourceappservice.bicep
Front Door (written, switched off)Off: the subscription refuses Front Door, so the Netlify edge stands in front todayoffmain.bicep
Application Insights with OpenTelemetryAzure Monitor1.6.0TheYard.Api.csproj
Communication Services emailAzure Communication Services1.1.0TheYard.Api.csproj
Azure Functions, Flex Consumption (clock, snapshot, tracker merger, Sheet mirror, ATS sweep, daily card)func-jobhunt-ss, Flex Consumption.NET 10 isolatedJobHunt.Functions.csproj
Bicep for every resource it deploysDeployed from the repository6 Bicep filesinfra/
Edge
EdgeNetlify edge in front of the siteNetlify edge networklive resourcenetlify.toml
Quality gatesevery push
Quality gatesevery pushxUnitGitHub Actions runners2.9.3TheYard.Tests.csproj
NetArchTest architecture rulesGitHub Actions runners1.4.5TheYard.Tests.csproj
VitestGitHub Actions runners5.0package.json
Playwright with axe accessibility checksGitHub Actions runners, against the built site1.63package.json
oxlint and PrettierGitHub Actions runners1.86 and 3.9package.json
Delivery and AIhow it ships
Delivery and AIhow it shipsGitHub Actions (azure-read, blog-post, ci, deploy-function, deploy-render, deploy, edge-branch, gate, nightly)GitHub9 workflowsworkflows/
Built with Claude Code under the test gateDeveloper machine, then the gateevery pushgate.yml
Claude Code on GitHub Actions runs the job hunt's nightly lanesGitHub Actions runners, on the Azure share4 AM and every 4 hnightly.yml

Generated, not typed. A build step reads the project files, package.json, the Dockerfiles, the Bicep and the workflows; a test fails CI when this table drifts from the repository.

How it is built

How it is run

Who and why