# Phase 00 — Bootstrap

Scaffold the .NET 10 Worker Service project and the test project. Wire up Generic Host, Serilog, Windows-Service registration, and `MbproxyOptions` POCOs bound via `IOptionsMonitor`. No proxy logic yet — the service starts, logs "ready", and stops cleanly.

**Depends on:** nothing. Must run alone.
**Parallel-safe with:** nothing. Phase 00 owns the initial `.csproj` and solution; subsequent phases append.

## Goal

Produce a minimal but production-shaped host that all subsequent phases plug into. The host must:

- Target `.NET 10` (`net10.0`), be registered as a Windows Service via `Microsoft.Extensions.Hosting.WindowsServices`, and also run as a console under `dotnet run` for local dev.
- Load `appsettings.json` with `reloadOnChange: true`, bind the `"Mbproxy"` section to typed POCOs, and expose them via `IOptionsMonitor<MbproxyOptions>`.
- Use Serilog with console + rolling-file sinks under `%ProgramData%\mbproxy\logs\` (configurable, but default that location).
- Set `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>` and `<Nullable>enable</Nullable>` in the csproj. These stay set forever.

## Outputs (files created in this phase)

```
Mbproxy.slnx
src/Mbproxy/Mbproxy.csproj
src/Mbproxy/Program.cs
src/Mbproxy/HostingExtensions.cs        # AddMbproxyOptions, AddMbproxySerilog
src/Mbproxy/Options/MbproxyOptions.cs
src/Mbproxy/Options/BcdTagOptions.cs
src/Mbproxy/Options/PlcOptions.cs
src/Mbproxy/Options/ConnectionOptions.cs
src/Mbproxy/Options/ResilienceOptions.cs
src/Mbproxy/Options/BcdTagListOptions.cs   # the Global + per-PLC Add/Remove DTOs
src/Mbproxy/Workers/HeartbeatWorker.cs     # one-line "service alive" worker; deleted by phase 03
src/Mbproxy/appsettings.json               # minimal default with empty Plcs array
tests/Mbproxy.Tests/Mbproxy.Tests.csproj
tests/Mbproxy.Tests/HostSmokeTests.cs
tests/Mbproxy.Tests/Options/MbproxyOptionsBindingTests.cs
.gitignore                                 # add bin/, obj/, .vs/, *.user, tests/sim/.venv/, %ProgramData%\mbproxy\
```

No other files. Phase 00 does NOT create:
- BCD codec types (phase 02)
- Proxy types (phase 03)
- Listener supervisor (phase 05)
- Status page (phase 07)

## Tasks

1. **Create `Mbproxy.slnx`** referencing the two csprojs.
2. **`src/Mbproxy/Mbproxy.csproj`** — `<Project Sdk="Microsoft.NET.Sdk.Worker">`, `TargetFramework=net10.0`, `OutputType=Exe`, `Nullable=enable`, `TreatWarningsAsErrors=true`, `ImplicitUsings=enable`. PackageReferences:
   - `Microsoft.Extensions.Hosting` (latest stable for .NET 10)
   - `Microsoft.Extensions.Hosting.WindowsServices`
   - `Serilog.Extensions.Hosting`
   - `Serilog.Settings.Configuration`
   - `Serilog.Sinks.Console`
   - `Serilog.Sinks.File`
   - `Polly` (referenced now so phase 04/05 don't have to touch this csproj for the package; usage is deferred)
3. **`Options/MbproxyOptions.cs`** and siblings — typed POCOs that mirror the appsettings schema in [`../design.md`](../design.md) → Configuration. Keep them plain DTOs (`public sealed class` with init-only properties). Use `IValidateOptions<MbproxyOptions>` for cross-field checks at the **schema** level only (no business rules like "duplicate addresses" — those move to phase 06 along with hot-reload).
4. **`HostingExtensions.cs`** — extension methods on `IHostApplicationBuilder` named `AddMbproxyOptions(IConfiguration)` and `AddMbproxySerilog(IConfiguration)`. Keep `Program.cs` thin: read config, call the two extensions, register `HeartbeatWorker`, run.
5. **`Program.cs`** — Generic Host with `.UseWindowsService()`. `await Host.CreateApplicationBuilder(args)...Build().RunAsync()`. Honour `--console` as a no-op flag for documentation symmetry with the design (the worker SDK + UseWindowsService combo already runs in console mode under `dotnet run`).
6. **`Workers/HeartbeatWorker.cs`** — `BackgroundService` that logs `mbproxy.startup.ready` once after `Task.Delay(100)` (so Serilog has flushed) and then idles. This worker is deleted in phase 03 when the real listener supervisor takes over; it exists so phase 00's smoke test has something to assert.
7. **`appsettings.json`** — minimal, valid against the POCOs, with `Plcs: []`. Include the full key shape (`BcdTags.Global`, `AdminPort`, `Connection`, `Resilience`) so future phases just fill in values.
8. **`tests/Mbproxy.Tests/Mbproxy.Tests.csproj`** — Microsoft.NET.Sdk, `TargetFramework=net10.0`, same `Nullable`/`TreatWarningsAsErrors`. ProjectReference to `src/Mbproxy/Mbproxy.csproj`. PackageReferences:
   - `Microsoft.NET.Test.Sdk`
   - `xunit` (v3 if a stable release exists; v2 otherwise — record the decision in the csproj comment)
   - `xunit.runner.visualstudio`
   - `Shouldly`
9. **`HostSmokeTests.cs`** — build the host with `Host.CreateApplicationBuilder` against a synthetic config, start it on a `CancellationTokenSource` with a short deadline, assert it logged `mbproxy.startup.ready` and shut down without unhandled exceptions.
10. **`MbproxyOptionsBindingTests.cs`** — bind a hand-written `Dictionary<string,string>` config source into `MbproxyOptions`, assert all fields populate correctly (including a `Plcs` entry with `BcdTags.Add` and `BcdTags.Remove`).

## Public surface declared in this phase

```csharp
namespace Mbproxy.Options;

public sealed class MbproxyOptions {
    public BcdTagListOptions BcdTags { get; init; } = new();
    public IReadOnlyList<PlcOptions> Plcs { get; init; } = [];
    public int AdminPort { get; init; } = 8080;
    public ConnectionOptions Connection { get; init; } = new();
    public ResilienceOptions Resilience { get; init; } = new();
}

public sealed class BcdTagListOptions {
    public IReadOnlyList<BcdTagOptions> Global { get; init; } = [];
}

public sealed class BcdTagOptions {
    public ushort Address { get; init; }
    public byte Width { get; init; }  // 16 or 32
}

public sealed class PlcOptions {
    public string Name { get; init; } = "";
    public int ListenPort { get; init; }
    public string Host { get; init; } = "";
    public PlcBcdOverrides? BcdTags { get; init; }
}

public sealed class PlcBcdOverrides {
    public IReadOnlyList<BcdTagOptions> Add { get; init; } = [];
    public IReadOnlyList<ushort> Remove { get; init; } = [];
}

public sealed class ConnectionOptions {
    public int BackendConnectTimeoutMs { get; init; } = 3000;
    public int BackendRequestTimeoutMs { get; init; } = 3000;
}

public sealed class ResilienceOptions {
    public RetryProfile BackendConnect { get; init; } = new() { MaxAttempts = 3, BackoffMs = [100, 500, 2000] };
    public RecoveryProfile ListenerRecovery { get; init; } = new() {
        InitialBackoffMs = [1000, 2000, 5000, 15000, 30000],
        SteadyStateMs = 30000,
    };
}

public sealed class RetryProfile {
    public int MaxAttempts { get; init; }
    public IReadOnlyList<int> BackoffMs { get; init; } = [];
}

public sealed class RecoveryProfile {
    public IReadOnlyList<int> InitialBackoffMs { get; init; } = [];
    public int SteadyStateMs { get; init; }
}
```

```csharp
namespace Mbproxy;

internal static class HostingExtensions {
    public static IHostApplicationBuilder AddMbproxyOptions(this IHostApplicationBuilder b);
    public static IHostApplicationBuilder AddMbproxySerilog(this IHostApplicationBuilder b);
}
```

```csharp
namespace Mbproxy.Workers;
internal sealed class HeartbeatWorker : BackgroundService { /* logs mbproxy.startup.ready */ }
```

No other public types in this phase.

## Tests required

### Unit (`Category = Unit`, default)

1. `MbproxyOptionsBinding_BindsGlobalBcdTags_From_appsettings`
2. `MbproxyOptionsBinding_BindsPerPlcAddAndRemove`
3. `MbproxyOptionsBinding_DefaultsAreApplied_WhenSectionMissing` (AdminPort=8080, Resilience defaults)
4. `MbproxyOptionsBinding_RejectsInvalidWidth` — IValidateOptions returns Fail for `Width != 16 && Width != 32`. Schema-level only; address-overlap validation is phase 06.
5. `HostSmoke_StartsAndStops_Cleanly_AndLogs_StartupReady` — uses a Serilog sink that captures events to memory; asserts the `mbproxy.startup.ready` event fired at Information.
6. `HostSmoke_ShutdownIsOrdered` — host responds to `StopAsync` within 2 s.

### E2E (`Category = E2E`)

None in this phase. The simulator harness is phase 01.

## Phase gate

- [ ] `dotnet build Mbproxy.slnx -c Debug` — zero warnings.
- [ ] `dotnet test --filter Category!=E2E` — all green, ≥6 tests.
- [ ] `dotnet run --project src/Mbproxy` — service starts, logs `mbproxy.startup.ready` to console within 5 s, exits cleanly on Ctrl-C.
- [ ] `appsettings.json` is a valid JSON document and parses into a populated `MbproxyOptions` instance via the test harness.
- [ ] [`../design.md`](../design.md) is unchanged (this phase introduces no new design decisions).
- [ ] Resource index entry for `docs/plan/00-bootstrap.md` is not needed (the plan README routes there).

## Out of scope

- BCD encode/decode logic (phase 02).
- TcpListener / Modbus framing / byte forwarding (phase 03).
- Polly retry pipelines (referenced as a NuGet, used starting in phase 04/05).
- Address-overlap / duplicate-port validation (phase 06).
- AdminPort HTTP endpoint (phase 07).
- Service install / uninstall scripts (phase 08).

## Notes for the subagent

- Do not create `README.md` for the tool root yet — that's a phase 08 deliverable when there's something installable to document.
- If the `xunit` v3 vs v2 question is unclear at implementation time, prefer v3 if available on NuGet — record the choice in a single-line comment at the top of the test csproj. Future phases must not silently switch.
- Use `LoggerMessage`-source-generated logging (`[LoggerMessage]`) for the heartbeat event so phases that add more log events can follow the same pattern. Set `EventId.Name = "mbproxy.startup.ready"`.
