diff --git a/docs/superpowers/plans/2026-03-19-nest-api.md b/docs/superpowers/plans/2026-03-19-nest-api.md
new file mode 100644
index 0000000..09c7f8c
--- /dev/null
+++ b/docs/superpowers/plans/2026-03-19-nest-api.md
@@ -0,0 +1,1039 @@
+# Nest API Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Create `OpenNest.Api` project with `NestRequest`/`NestResponse` facade, rename internal types, and standardize file extensions.
+
+**Architecture:** New `OpenNest.Api` class library wraps Engine + IO + Timing behind a stateless `NestRunner.RunAsync()`. Renames `NestResult` → `OptimizationResult`, replaces old `CutParameters` in Core with new `OpenNest.Api` namespace version, and renames `.opnest` → `.nest` across the solution.
+
+**Tech Stack:** .NET 8, C#, xUnit, System.IO.Compression (ZIP), System.Text.Json
+
+**Spec:** `docs/superpowers/specs/2026-03-19-nest-api-design.md`
+
+**Out of scope:** Consumer integration (MCP `nest_and_quote` tool, Console app simplification) — covered in a follow-up plan.
+
+---
+
+## File Map
+
+### New Files (OpenNest.Api)
+| File | Responsibility |
+|------|---------------|
+| `OpenNest.Api/OpenNest.Api.csproj` | Project file referencing Core, Engine, IO |
+| `OpenNest.Api/NestStrategy.cs` | Strategy enum (Auto only for now) |
+| `OpenNest.Api/NestRequestPart.cs` | Single part input (DXF path + quantity + rotation + priority) |
+| `OpenNest.Api/NestRequest.cs` | Immutable nesting request |
+| `OpenNest.Api/NestResponse.cs` | Immutable nesting response with SaveAsync/LoadAsync |
+| `OpenNest.Api/NestRunner.cs` | Stateless orchestrator |
+
+### New Files (Tests)
+| File | Responsibility |
+|------|---------------|
+| `OpenNest.Tests/Api/CutParametersTests.cs` | CutParameters defaults and construction |
+| `OpenNest.Tests/Api/NestRequestTests.cs` | Request validation and immutability |
+| `OpenNest.Tests/Api/NestRunnerTests.cs` | Runner integration tests (DXF import → nest → response) |
+| `OpenNest.Tests/Api/NestResponsePersistenceTests.cs` | SaveAsync/LoadAsync round-trip |
+
+### Modified Files (Renames)
+| File | Change |
+|------|--------|
+| `OpenNest.Core/CutParameters.cs` | Replace contents: change namespace to `OpenNest.Api`, add new properties |
+| `OpenNest.Core/Timing.cs:70` | Add `using OpenNest.Api;` |
+| `OpenNest.Engine/INestOptimizer.cs:10,34` | Rename `NestResult` → `OptimizationResult` |
+| `OpenNest.Engine/SimulatedAnnealing.cs:20,31,108` | Update all `NestResult` references |
+| `OpenNest/OpenNest.csproj` | Add ProjectReference to OpenNest.Api |
+| `OpenNest/Forms/CutParametersForm.cs` | Add `using OpenNest.Api;` |
+| `OpenNest/Forms/TimingForm.cs` | Add `using OpenNest.Api;` |
+| `OpenNest/Forms/EditNestForm.cs` | Add `using OpenNest.Api;` |
+| `OpenNest.IO/NestFormat.cs:8-9` | Change `.opnest` → `.nest` |
+| `OpenNest.IO/NestWriter.cs` | Add `Write(Stream)` overload |
+| `OpenNest.Console/Program.cs:194,390,393,395,403` | Update `.opnest` string literals to `.nest` |
+| `OpenNest.Mcp/Tools/TestTools.cs:20,23` | Update `.opnest` references |
+| `OpenNest.Mcp/Tools/InputTools.cs:24-25` | Update `.opnest` references |
+| `OpenNest.Training/Program.cs:302` | Update `.opnest` reference |
+| `OpenNest.Tests/OpenNest.Tests.csproj` | Add ProjectReference to OpenNest.Api |
+| `OpenNest.sln` | Add OpenNest.Api project |
+
+---
+
+### Task 1: Create OpenNest.Api Project Skeleton
+
+**Files:**
+- Create: `OpenNest.Api/OpenNest.Api.csproj`
+- Modify: `OpenNest.sln`
+- Modify: `OpenNest.Tests/OpenNest.Tests.csproj`
+
+- [ ] **Step 1: Create the project file**
+
+```xml
+
+
+
+ net8.0-windows
+ OpenNest.Api
+ OpenNest.Api
+
+
+
+
+
+
+
+```
+
+- [ ] **Step 2: Add project to solution**
+
+```bash
+cd "C:/Users/aisaacs/Desktop/Projects/OpenNest"
+dotnet sln OpenNest.sln add OpenNest.Api/OpenNest.Api.csproj
+```
+
+- [ ] **Step 3: Add test project reference**
+
+Add to `OpenNest.Tests/OpenNest.Tests.csproj` inside the `` with other ProjectReferences:
+
+```xml
+
+```
+
+- [ ] **Step 4: Build to verify**
+
+```bash
+dotnet build OpenNest.sln
+```
+
+Expected: Build succeeds with no errors.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add OpenNest.Api/OpenNest.Api.csproj OpenNest.sln OpenNest.Tests/OpenNest.Tests.csproj
+git commit -m "chore: add OpenNest.Api project skeleton"
+```
+
+---
+
+### Task 2: Replace CutParameters in Core with OpenNest.Api Namespace
+
+The existing `OpenNest.CutParameters` in Core gets replaced in-place: same physical file, but new namespace (`OpenNest.Api`) and additional properties. This avoids circular references (Core doesn't reference Api) while giving all consumers the `OpenNest.Api.CutParameters` type.
+
+**Files:**
+- Modify: `OpenNest.Core/CutParameters.cs` (replace contents)
+- Modify: `OpenNest.Core/Timing.cs` (add using)
+- Modify: `OpenNest/OpenNest.csproj` (add Api reference)
+- Modify: `OpenNest/Forms/CutParametersForm.cs` (add using)
+- Modify: `OpenNest/Forms/TimingForm.cs` (add using)
+- Modify: `OpenNest/Forms/EditNestForm.cs` (add using)
+- Create: `OpenNest.Tests/Api/CutParametersTests.cs`
+
+- [ ] **Step 1: Write the failing test**
+
+```csharp
+// OpenNest.Tests/Api/CutParametersTests.cs
+using OpenNest.Api;
+
+namespace OpenNest.Tests.Api;
+
+public class CutParametersTests
+{
+ [Fact]
+ public void Default_HasExpectedValues()
+ {
+ var cp = CutParameters.Default;
+
+ Assert.Equal(100, cp.Feedrate);
+ Assert.Equal(300, cp.RapidTravelRate);
+ Assert.Equal(TimeSpan.FromSeconds(0.5), cp.PierceTime);
+ Assert.Equal(Units.Inches, cp.Units);
+ }
+
+ [Fact]
+ public void Properties_AreSettable()
+ {
+ var cp = new CutParameters
+ {
+ Feedrate = 200,
+ RapidTravelRate = 500,
+ PierceTime = TimeSpan.FromSeconds(1.0),
+ LeadInLength = 0.25,
+ PostProcessor = "CL-707",
+ Units = Units.Millimeters
+ };
+
+ Assert.Equal(200, cp.Feedrate);
+ Assert.Equal(0.25, cp.LeadInLength);
+ Assert.Equal("CL-707", cp.PostProcessor);
+ }
+}
+```
+
+- [ ] **Step 2: Replace CutParameters.cs contents**
+
+Replace the entire contents of `OpenNest.Core/CutParameters.cs`:
+
+```csharp
+using System;
+
+namespace OpenNest.Api;
+
+public class CutParameters
+{
+ public double Feedrate { get; set; }
+ public double RapidTravelRate { get; set; }
+ public TimeSpan PierceTime { get; set; }
+ public double LeadInLength { get; set; }
+ public string PostProcessor { get; set; }
+ public Units Units { get; set; }
+
+ public static CutParameters Default => new()
+ {
+ Feedrate = 100,
+ RapidTravelRate = 300,
+ PierceTime = TimeSpan.FromSeconds(0.5),
+ Units = OpenNest.Units.Inches
+ };
+}
+```
+
+**Note:** Properties use `{ get; set; }` (not `init`) to match existing mutation patterns in the codebase. `Units` in the `Default` property is fully qualified as `OpenNest.Units.Inches` to avoid ambiguity with the `Units` property.
+
+- [ ] **Step 3: Update Timing.cs**
+
+Add `using OpenNest.Api;` to the top of `OpenNest.Core/Timing.cs`. The `CalculateTime(TimingInfo info, CutParameters cutParams)` method at line 70 now resolves to the new type — same property names, no body changes needed.
+
+- [ ] **Step 4: Add Api reference to WinForms project**
+
+Add to `OpenNest/OpenNest.csproj` inside the `` with other ProjectReferences:
+
+```xml
+
+```
+
+- [ ] **Step 5: Update WinForms forms**
+
+Add `using OpenNest.Api;` to each of these files:
+- `OpenNest/Forms/CutParametersForm.cs`
+- `OpenNest/Forms/TimingForm.cs`
+- `OpenNest/Forms/EditNestForm.cs`
+
+No other changes — same property names, same construction pattern.
+
+- [ ] **Step 6: Build and run tests**
+
+```bash
+dotnet test OpenNest.Tests --filter "FullyQualifiedName~CutParametersTests"
+```
+
+Expected: 2 tests pass. Full build also succeeds with no errors.
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add OpenNest.Core/CutParameters.cs OpenNest.Core/Timing.cs OpenNest/OpenNest.csproj OpenNest/Forms/CutParametersForm.cs OpenNest/Forms/TimingForm.cs OpenNest/Forms/EditNestForm.cs OpenNest.Tests/Api/CutParametersTests.cs
+git commit -m "refactor: move CutParameters to OpenNest.Api namespace with new properties"
+```
+
+---
+
+### Task 3: NestStrategy, NestRequestPart, and NestRequest
+
+**Files:**
+- Create: `OpenNest.Api/NestStrategy.cs`
+- Create: `OpenNest.Api/NestRequestPart.cs`
+- Create: `OpenNest.Api/NestRequest.cs`
+- Create: `OpenNest.Tests/Api/NestRequestTests.cs`
+
+- [ ] **Step 1: Write the failing tests**
+
+```csharp
+// OpenNest.Tests/Api/NestRequestTests.cs
+using OpenNest.Api;
+using OpenNest.Geometry;
+
+namespace OpenNest.Tests.Api;
+
+public class NestRequestTests
+{
+ [Fact]
+ public void Default_Request_HasSensibleDefaults()
+ {
+ var request = new NestRequest();
+
+ Assert.Empty(request.Parts);
+ Assert.Equal(60, request.SheetSize.Width);
+ Assert.Equal(120, request.SheetSize.Length);
+ Assert.Equal("Steel, A1011 HR", request.Material);
+ Assert.Equal(0.06, request.Thickness);
+ Assert.Equal(0.1, request.Spacing);
+ Assert.Equal(NestStrategy.Auto, request.Strategy);
+ Assert.NotNull(request.Cutting);
+ }
+
+ [Fact]
+ public void Parts_Accessible_AfterConstruction()
+ {
+ var request = new NestRequest
+ {
+ Parts = [new NestRequestPart { DxfPath = "test.dxf", Quantity = 5 }]
+ };
+
+ Assert.Single(request.Parts);
+ Assert.Equal("test.dxf", request.Parts[0].DxfPath);
+ Assert.Equal(5, request.Parts[0].Quantity);
+ }
+
+ [Fact]
+ public void NestRequestPart_Defaults()
+ {
+ var part = new NestRequestPart { DxfPath = "part.dxf" };
+
+ Assert.Equal(1, part.Quantity);
+ Assert.True(part.AllowRotation);
+ Assert.Equal(0, part.Priority);
+ }
+}
+```
+
+- [ ] **Step 2: Run tests to verify they fail**
+
+```bash
+dotnet test OpenNest.Tests --filter "FullyQualifiedName~NestRequestTests" --no-build
+```
+
+Expected: FAIL — types do not exist.
+
+- [ ] **Step 3: Write the implementations**
+
+```csharp
+// OpenNest.Api/NestStrategy.cs
+namespace OpenNest.Api;
+
+public enum NestStrategy { Auto }
+```
+
+```csharp
+// OpenNest.Api/NestRequestPart.cs
+namespace OpenNest.Api;
+
+public class NestRequestPart
+{
+ public string DxfPath { get; init; }
+ public int Quantity { get; init; } = 1;
+ public bool AllowRotation { get; init; } = true;
+ public int Priority { get; init; } = 0;
+}
+```
+
+```csharp
+// OpenNest.Api/NestRequest.cs
+using System.Collections.Generic;
+using OpenNest.Geometry;
+
+namespace OpenNest.Api;
+
+public class NestRequest
+{
+ public IReadOnlyList Parts { get; init; } = [];
+ public Size SheetSize { get; init; } = new(60, 120);
+ public string Material { get; init; } = "Steel, A1011 HR";
+ public double Thickness { get; init; } = 0.06;
+ public double Spacing { get; init; } = 0.1;
+ public NestStrategy Strategy { get; init; } = NestStrategy.Auto;
+ public CutParameters Cutting { get; init; } = CutParameters.Default;
+}
+```
+
+- [ ] **Step 4: Build and run tests**
+
+```bash
+dotnet test OpenNest.Tests --filter "FullyQualifiedName~NestRequestTests"
+```
+
+Expected: 3 tests pass.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add OpenNest.Api/NestStrategy.cs OpenNest.Api/NestRequestPart.cs OpenNest.Api/NestRequest.cs OpenNest.Tests/Api/NestRequestTests.cs
+git commit -m "feat(api): add NestStrategy, NestRequestPart, NestRequest"
+```
+
+---
+
+### Task 4: Rename NestResult → OptimizationResult
+
+**Files:**
+- Modify: `OpenNest.Engine/INestOptimizer.cs` (class name + return type)
+- Modify: `OpenNest.Engine/SimulatedAnnealing.cs` (all references)
+
+- [ ] **Step 1: Rename in INestOptimizer.cs**
+
+In `OpenNest.Engine/INestOptimizer.cs`:
+- Line 10: `public class NestResult` → `public class OptimizationResult`
+- Line 34: return type `NestResult` → `OptimizationResult`
+
+- [ ] **Step 2: Rename in SimulatedAnnealing.cs**
+
+In `OpenNest.Engine/SimulatedAnnealing.cs`:
+- Line 20: return type `NestResult` → `OptimizationResult`
+- Line 31: `new NestResult` → `new OptimizationResult`
+- Line 108: `new NestResult` → `new OptimizationResult`
+
+- [ ] **Step 3: Build and run all tests**
+
+```bash
+dotnet build OpenNest.sln && dotnet test OpenNest.Tests
+```
+
+Expected: Build succeeds, all existing tests pass. If there are additional `NestResult` references the build will reveal them — fix any that appear.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add OpenNest.Engine/INestOptimizer.cs OpenNest.Engine/SimulatedAnnealing.cs
+git commit -m "refactor(engine): rename NestResult to OptimizationResult"
+```
+
+---
+
+### Task 5: Rename .opnest → .nest File Extension
+
+**Files:**
+- Modify: `OpenNest.IO/NestFormat.cs:8-9`
+- Modify: `OpenNest.Console/Program.cs:194,390,393,395,403`
+- Modify: `OpenNest.Mcp/Tools/TestTools.cs:20,23`
+- Modify: `OpenNest.Mcp/Tools/InputTools.cs:24-25`
+- Modify: `OpenNest.Training/Program.cs:302`
+
+- [ ] **Step 1: Update NestFormat.cs**
+
+In `OpenNest.IO/NestFormat.cs`:
+- Line 8: `".opnest"` → `".nest"`
+- Line 9: `"Nest Files (*.opnest)|*.opnest"` → `"Nest Files (*.nest)|*.nest"`
+
+- [ ] **Step 2: Update Console/Program.cs**
+
+Replace all `.opnest` string literals with `.nest`:
+- Line 194: `(.opnest)` → `(.nest)`
+- Line 390: `.opnest` → `.nest`
+- Line 393: `` → ``
+- Line 395: `` → ``
+- Line 403: `-result.opnest` → `-result.nest`
+
+- [ ] **Step 3: Update MCP tools**
+
+In `OpenNest.Mcp/Tools/TestTools.cs`:
+- Line 20: change `.opnest` → `.nest` in default path
+- Line 23: change `.opnest` → `.nest` in description
+
+In `OpenNest.Mcp/Tools/InputTools.cs`:
+- Line 24: change `.opnest` → `.nest` in description
+- Line 25: change `.opnest` → `.nest` in description
+
+- [ ] **Step 4: Update Training/Program.cs**
+
+- Line 302: change `.opnest` → `.nest`
+
+- [ ] **Step 5: Verify no remaining .opnest references in code**
+
+```bash
+grep -r "\.opnest" --include="*.cs" "C:/Users/aisaacs/Desktop/Projects/OpenNest"
+```
+
+Expected: No results.
+
+- [ ] **Step 6: Build to verify**
+
+```bash
+dotnet build OpenNest.sln
+```
+
+Expected: Build succeeds.
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add OpenNest.IO/NestFormat.cs OpenNest.Console/Program.cs OpenNest.Mcp/Tools/TestTools.cs OpenNest.Mcp/Tools/InputTools.cs OpenNest.Training/Program.cs
+git commit -m "refactor: rename .opnest file extension to .nest"
+```
+
+---
+
+### Task 6: Add NestWriter.Write(Stream) Overload
+
+The existing `NestWriter.Write(string)` creates a `FileStream` and `ZipArchive` internally. We need a `Write(Stream)` overload so `NestResponse.SaveAsync` can write the `.nest` payload to a `MemoryStream` for embedding inside the `.nestquote` ZIP.
+
+**Files:**
+- Modify: `OpenNest.IO/NestWriter.cs`
+
+- [ ] **Step 1: Add Write(Stream) overload**
+
+In `OpenNest.IO/NestWriter.cs`, add a new method after the existing `Write(string)` method (after line 43):
+
+```csharp
+public bool Write(Stream stream)
+{
+ nest.DateLastModified = DateTime.Now;
+ SetDrawingIds();
+
+ using var zipArchive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true);
+
+ WriteNestJson(zipArchive);
+ WritePrograms(zipArchive);
+ WriteBestFits(zipArchive);
+
+ return true;
+}
+```
+
+**Note:** `leaveOpen: true` is critical — without it, `ZipArchive.Dispose()` would close the underlying `MemoryStream`, preventing the caller from reading it afterwards.
+
+- [ ] **Step 2: Refactor Write(string) to delegate**
+
+Replace the existing `Write(string)` method body to delegate to the new overload:
+
+```csharp
+public bool Write(string file)
+{
+ using var fileStream = new FileStream(file, FileMode.Create);
+ return Write(fileStream);
+}
+```
+
+- [ ] **Step 3: Build and run all tests**
+
+```bash
+dotnet build OpenNest.sln && dotnet test OpenNest.Tests
+```
+
+Expected: Build succeeds, all existing tests pass (NestWriter behavior unchanged for file-based callers).
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add OpenNest.IO/NestWriter.cs
+git commit -m "feat(io): add NestWriter.Write(Stream) overload"
+```
+
+---
+
+### Task 7: NestResponse Type with SaveAsync/LoadAsync
+
+**Files:**
+- Create: `OpenNest.Api/NestResponse.cs`
+- Create: `OpenNest.Tests/Api/NestResponsePersistenceTests.cs`
+
+- [ ] **Step 1: Write the round-trip test**
+
+```csharp
+// OpenNest.Tests/Api/NestResponsePersistenceTests.cs
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using OpenNest.Api;
+using OpenNest.Geometry;
+
+namespace OpenNest.Tests.Api;
+
+public class NestResponsePersistenceTests
+{
+ [Fact]
+ public async Task SaveAsync_LoadAsync_RoundTrips()
+ {
+ var nest = new Nest("test-nest");
+ var plate = new Plate(new Size(60, 120));
+ nest.Plates.Add(plate);
+
+ var request = new NestRequest
+ {
+ Parts = [new NestRequestPart { DxfPath = "test.dxf", Quantity = 5 }],
+ SheetSize = new Size(60, 120),
+ Material = "Steel",
+ Thickness = 0.125,
+ Spacing = 0.1
+ };
+
+ var original = new NestResponse
+ {
+ SheetCount = 1,
+ Utilization = 0.75,
+ CutTime = TimeSpan.FromMinutes(12.5),
+ Elapsed = TimeSpan.FromSeconds(3.2),
+ Nest = nest,
+ Request = request
+ };
+
+ var path = Path.Combine(Path.GetTempPath(), $"test-{Guid.NewGuid()}.nestquote");
+
+ try
+ {
+ await original.SaveAsync(path);
+ var loaded = await NestResponse.LoadAsync(path);
+
+ Assert.Equal(original.SheetCount, loaded.SheetCount);
+ Assert.Equal(original.Utilization, loaded.Utilization, precision: 4);
+ Assert.Equal(original.CutTime, loaded.CutTime);
+ Assert.Equal(original.Elapsed, loaded.Elapsed);
+
+ Assert.Equal(original.Request.Material, loaded.Request.Material);
+ Assert.Equal(original.Request.Thickness, loaded.Request.Thickness);
+ Assert.Equal(original.Request.Parts.Count, loaded.Request.Parts.Count);
+ Assert.Equal(original.Request.Parts[0].DxfPath, loaded.Request.Parts[0].DxfPath);
+ Assert.Equal(original.Request.Parts[0].Quantity, loaded.Request.Parts[0].Quantity);
+
+ Assert.NotNull(loaded.Nest);
+ Assert.Equal(1, loaded.Nest.Plates.Count);
+ }
+ finally
+ {
+ File.Delete(path);
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Write NestResponse implementation**
+
+```csharp
+// OpenNest.Api/NestResponse.cs
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Text.Json;
+using System.Threading.Tasks;
+using OpenNest.IO;
+
+namespace OpenNest.Api;
+
+public class NestResponse
+{
+ public int SheetCount { get; init; }
+ public double Utilization { get; init; }
+ public TimeSpan CutTime { get; init; }
+ public TimeSpan Elapsed { get; init; }
+ public Nest Nest { get; init; }
+ public NestRequest Request { get; init; }
+
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = true,
+ IncludeFields = true // Required for OpenNest.Geometry.Size (public fields)
+ };
+
+ public async Task SaveAsync(string path)
+ {
+ using var fs = new FileStream(path, FileMode.Create);
+ using var zip = new ZipArchive(fs, ZipArchiveMode.Create);
+
+ // Write request.json
+ var requestEntry = zip.CreateEntry("request.json");
+ await using (var stream = requestEntry.Open())
+ {
+ await JsonSerializer.SerializeAsync(stream, Request, JsonOptions);
+ }
+
+ // Write response.json (metrics only)
+ var metrics = new
+ {
+ SheetCount,
+ Utilization,
+ CutTimeTicks = CutTime.Ticks,
+ ElapsedTicks = Elapsed.Ticks
+ };
+ var responseEntry = zip.CreateEntry("response.json");
+ await using (var stream = responseEntry.Open())
+ {
+ await JsonSerializer.SerializeAsync(stream, metrics, JsonOptions);
+ }
+
+ // Write embedded nest.nest via NestWriter → MemoryStream → ZIP entry
+ var nestEntry = zip.CreateEntry("nest.nest");
+ using var nestMs = new MemoryStream();
+ var writer = new NestWriter(Nest);
+ writer.Write(nestMs);
+ nestMs.Position = 0;
+ await using (var stream = nestEntry.Open())
+ {
+ await nestMs.CopyToAsync(stream);
+ }
+ }
+
+ public static async Task LoadAsync(string path)
+ {
+ using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
+ using var zip = new ZipArchive(fs, ZipArchiveMode.Read);
+
+ // Read request.json
+ var requestEntry = zip.GetEntry("request.json");
+ NestRequest request;
+ await using (var stream = requestEntry!.Open())
+ {
+ request = await JsonSerializer.DeserializeAsync(stream, JsonOptions);
+ }
+
+ // Read response.json
+ var responseEntry = zip.GetEntry("response.json");
+ JsonElement metricsJson;
+ await using (var stream = responseEntry!.Open())
+ {
+ metricsJson = await JsonSerializer.DeserializeAsync(stream, JsonOptions);
+ }
+
+ // Read embedded nest.nest via NestReader(Stream)
+ var nestEntry = zip.GetEntry("nest.nest");
+ Nest nest;
+ using (var nestMs = new MemoryStream())
+ {
+ await using (var stream = nestEntry!.Open())
+ {
+ await stream.CopyToAsync(nestMs);
+ }
+ nestMs.Position = 0;
+ var reader = new NestReader(nestMs);
+ nest = reader.Read();
+ }
+
+ return new NestResponse
+ {
+ SheetCount = metricsJson.GetProperty("sheetCount").GetInt32(),
+ Utilization = metricsJson.GetProperty("utilization").GetDouble(),
+ CutTime = TimeSpan.FromTicks(metricsJson.GetProperty("cutTimeTicks").GetInt64()),
+ Elapsed = TimeSpan.FromTicks(metricsJson.GetProperty("elapsedTicks").GetInt64()),
+ Nest = nest,
+ Request = request
+ };
+ }
+}
+```
+
+**Key details:**
+- `IncludeFields = true` in `JsonOptions` — required because `OpenNest.Geometry.Size` is a struct with public fields (`Width`, `Length`), not properties. Without this, Size serializes as `{}`.
+- `NestReader(Stream)` constructor exists at `OpenNest.IO/NestReader.cs:26`. Its `Read()` method disposes the stream, which is fine since we use a separate `MemoryStream`.
+- `NestWriter.Write(Stream)` was added in Task 6.
+
+- [ ] **Step 3: Build and run tests**
+
+```bash
+dotnet test OpenNest.Tests --filter "FullyQualifiedName~NestResponsePersistenceTests"
+```
+
+Expected: 1 test passes.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add OpenNest.Api/NestResponse.cs OpenNest.Tests/Api/NestResponsePersistenceTests.cs
+git commit -m "feat(api): add NestResponse with SaveAsync/LoadAsync"
+```
+
+---
+
+### Task 8: NestRunner Implementation
+
+**Files:**
+- Create: `OpenNest.Api/NestRunner.cs`
+- Create: `OpenNest.Tests/Api/NestRunnerTests.cs`
+
+- [ ] **Step 1: Write the failing tests**
+
+```csharp
+// OpenNest.Tests/Api/NestRunnerTests.cs
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using OpenNest.Api;
+using OpenNest.Converters;
+using OpenNest.Geometry;
+using OpenNest.IO;
+
+namespace OpenNest.Tests.Api;
+
+public class NestRunnerTests
+{
+ [Fact]
+ public async Task RunAsync_SinglePart_ProducesResponse()
+ {
+ var dxfPath = CreateTempSquareDxf(2, 2);
+
+ try
+ {
+ var request = new NestRequest
+ {
+ Parts = [new NestRequestPart { DxfPath = dxfPath, Quantity = 4 }],
+ SheetSize = new Size(10, 10),
+ Spacing = 0.1
+ };
+
+ var response = await NestRunner.RunAsync(request);
+
+ Assert.NotNull(response);
+ Assert.NotNull(response.Nest);
+ Assert.True(response.SheetCount >= 1);
+ Assert.True(response.Utilization > 0);
+ Assert.Equal(request, response.Request);
+ }
+ finally
+ {
+ File.Delete(dxfPath);
+ }
+ }
+
+ [Fact]
+ public async Task RunAsync_BadDxfPath_Throws()
+ {
+ var request = new NestRequest
+ {
+ Parts = [new NestRequestPart { DxfPath = "nonexistent.dxf", Quantity = 1 }]
+ };
+
+ await Assert.ThrowsAsync(
+ () => NestRunner.RunAsync(request));
+ }
+
+ [Fact]
+ public async Task RunAsync_EmptyParts_Throws()
+ {
+ var request = new NestRequest { Parts = [] };
+
+ await Assert.ThrowsAsync(
+ () => NestRunner.RunAsync(request));
+ }
+
+ private static string CreateTempSquareDxf(double width, double height)
+ {
+ var shape = new Shape();
+ shape.Entities.Add(new Line(new Vector(0, 0), new Vector(width, 0)));
+ shape.Entities.Add(new Line(new Vector(width, 0), new Vector(width, height)));
+ shape.Entities.Add(new Line(new Vector(width, height), new Vector(0, height)));
+ shape.Entities.Add(new Line(new Vector(0, height), new Vector(0, 0)));
+
+ var pgm = ConvertGeometry.ToProgram(shape);
+ var path = Path.Combine(Path.GetTempPath(), $"test-{Guid.NewGuid()}.dxf");
+
+ var exporter = new DxfExporter();
+ exporter.ExportProgram(pgm, path);
+
+ return path;
+ }
+}
+```
+
+**API notes:**
+- `Shape()` — parameterless constructor, then add to `shape.Entities` list
+- `ConvertGeometry.ToProgram(Shape)` — takes a Shape directly
+- `DxfExporter.ExportProgram(Program, string)` — exports a program to a DXF file path
+
+- [ ] **Step 2: Run tests to verify they fail**
+
+```bash
+dotnet test OpenNest.Tests --filter "FullyQualifiedName~NestRunnerTests" --no-build
+```
+
+Expected: FAIL — `NestRunner` does not exist.
+
+- [ ] **Step 3: Write NestRunner implementation**
+
+```csharp
+// OpenNest.Api/NestRunner.cs
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using OpenNest.Converters;
+using OpenNest.Geometry;
+using OpenNest.IO;
+
+namespace OpenNest.Api;
+
+public static class NestRunner
+{
+ public static Task RunAsync(
+ NestRequest request,
+ IProgress progress = null,
+ CancellationToken token = default)
+ {
+ if (request.Parts.Count == 0)
+ throw new ArgumentException("Request must contain at least one part.", nameof(request));
+
+ var sw = Stopwatch.StartNew();
+
+ // 1. Import DXFs → Drawings
+ var drawings = new List();
+ var importer = new DxfImporter();
+
+ foreach (var part in request.Parts)
+ {
+ if (!File.Exists(part.DxfPath))
+ throw new FileNotFoundException($"DXF file not found: {part.DxfPath}", part.DxfPath);
+
+ if (!importer.GetGeometry(part.DxfPath, out var geometry) || geometry.Count == 0)
+ throw new InvalidOperationException($"Failed to import DXF: {part.DxfPath}");
+
+ var pgm = ConvertGeometry.ToProgram(geometry);
+ var name = Path.GetFileNameWithoutExtension(part.DxfPath);
+ var drawing = new Drawing(name);
+ drawing.Program = pgm;
+ drawings.Add(drawing);
+ }
+
+ // 2. Build NestItems
+ var items = new List();
+ for (var i = 0; i < request.Parts.Count; i++)
+ {
+ var part = request.Parts[i];
+ items.Add(new NestItem
+ {
+ Drawing = drawings[i],
+ Quantity = part.Quantity,
+ Priority = part.Priority,
+ StepAngle = part.AllowRotation ? 0 : OpenNest.Math.Angle.TwoPI,
+ });
+ }
+
+ // 3. Multi-plate loop
+ var nest = new Nest();
+ var remaining = items.Select(item => item.Quantity).ToList();
+
+ while (remaining.Any(q => q > 0))
+ {
+ token.ThrowIfCancellationRequested();
+
+ var plate = new Plate(request.SheetSize)
+ {
+ Thickness = request.Thickness,
+ PartSpacing = request.Spacing
+ };
+
+ // Build items for this pass with remaining quantities
+ var passItems = new List();
+ for (var i = 0; i < items.Count; i++)
+ {
+ if (remaining[i] <= 0) continue;
+ passItems.Add(new NestItem
+ {
+ Drawing = items[i].Drawing,
+ Quantity = remaining[i],
+ Priority = items[i].Priority,
+ StepAngle = items[i].StepAngle,
+ });
+ }
+
+ // Run engine
+ var engine = NestEngineRegistry.Create(plate);
+ var parts = engine.Nest(passItems, progress, token);
+
+ if (parts.Count == 0)
+ break; // No progress — part doesn't fit on fresh sheet
+
+ // Add parts to plate and nest
+ foreach (var p in parts)
+ plate.Parts.Add(p);
+
+ nest.Plates.Add(plate);
+
+ // Deduct placed quantities
+ foreach (var p in parts)
+ {
+ var idx = drawings.IndexOf(p.BaseDrawing);
+ if (idx >= 0)
+ remaining[idx]--;
+ }
+ }
+
+ // 4. Compute timing (placeholder — Timing will be reworked later)
+ var timingInfo = Timing.GetTimingInfo(nest);
+ var cutTime = Timing.CalculateTime(timingInfo, request.Cutting);
+
+ sw.Stop();
+
+ // 5. Build response
+ var response = new NestResponse
+ {
+ SheetCount = nest.Plates.Count,
+ Utilization = nest.Plates.Count > 0
+ ? nest.Plates.Average(p => p.Utilization())
+ : 0,
+ CutTime = cutTime,
+ Elapsed = sw.Elapsed,
+ Nest = nest,
+ Request = request
+ };
+
+ return Task.FromResult(response);
+ }
+}
+```
+
+**Key API details:**
+- `Drawing(string name)` constructor (confirmed in `NestReader.cs:81`), then set `drawing.Program = pgm`
+- `Part.BaseDrawing` (public readonly field, confirmed in `Part.cs:24` and `NestWriter.cs:154`)
+- `ConvertGeometry.ToProgram(IList)` for the DXF geometry list
+- `engine.Nest(List, IProgress, CancellationToken)` returns `List`
+- Method returns `Task.FromResult` since the actual work is synchronous (engine is CPU-bound, not async). The `Task` return type keeps the API async-friendly for future use.
+
+- [ ] **Step 4: Build and run tests**
+
+```bash
+dotnet test OpenNest.Tests --filter "FullyQualifiedName~NestRunnerTests"
+```
+
+Expected: 3 tests pass.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add OpenNest.Api/NestRunner.cs OpenNest.Tests/Api/NestRunnerTests.cs
+git commit -m "feat(api): add NestRunner with multi-plate loop"
+```
+
+---
+
+### Task 9: Full Integration Test and Final Verification
+
+**Files:**
+- None new — verification only
+
+- [ ] **Step 1: Run full solution build**
+
+```bash
+cd "C:/Users/aisaacs/Desktop/Projects/OpenNest"
+dotnet build OpenNest.sln
+```
+
+Expected: Build succeeds with no errors.
+
+- [ ] **Step 2: Run all tests**
+
+```bash
+dotnet test OpenNest.Tests
+```
+
+Expected: All tests pass — both existing and new Api tests.
+
+- [ ] **Step 3: Verify no remaining .opnest in code**
+
+```bash
+grep -r "\.opnest" --include="*.cs" "C:/Users/aisaacs/Desktop/Projects/OpenNest"
+```
+
+Expected: No results.
+
+- [ ] **Step 4: Verify no remaining old NestResult in code**
+
+```bash
+grep -r "\bNestResult\b" --include="*.cs" "C:/Users/aisaacs/Desktop/Projects/OpenNest" | grep -v "StripNestResult" | grep -v "OptimizationResult" | grep -v "NestResponse"
+```
+
+Expected: No results (StripNestResult is a separate internal type, fine to keep).
+
+- [ ] **Step 5: Verify CutParameters namespace**
+
+```bash
+grep -rn "namespace OpenNest;" --include="CutParameters.cs" "C:/Users/aisaacs/Desktop/Projects/OpenNest"
+```
+
+Expected: No results — the file should have `namespace OpenNest.Api;`.