diff --git a/OpenNest.Tests/CuttingPlanning/ProductionCorpusSweepTests.cs b/OpenNest.Tests/CuttingPlanning/ProductionCorpusSweepTests.cs
new file mode 100644
index 00000000..87f59d00
--- /dev/null
+++ b/OpenNest.Tests/CuttingPlanning/ProductionCorpusSweepTests.cs
@@ -0,0 +1,232 @@
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Text.Json;
+using OpenNest.CNC.CuttingStrategy;
+using OpenNest.Data;
+using OpenNest.Diagnostics;
+using OpenNest.Engine.CuttingPlanning;
+using OpenNest.IO;
+using Xunit.Abstractions;
+
+namespace OpenNest.Tests.CuttingPlanning;
+
+///
+/// Plans every occupied plate of every .nest file in a local folder the way Plan Cutting
+/// does after clearing its lead-ins and cut-off parts, and writes one JSON line per plate with
+/// its outcome, warnings, expansions and time. A survey, not an assertion: it never fails on an
+/// outcome. Opt-in and private: the folder holds production nests, so neither it nor the report
+/// belongs in the repository. Set to the folder; the report goes to
+/// , or sweep.jsonl beside the nests.
+///
+/// A plate plans with its own saved cutting parameters when it has them. Otherwise it uses the
+/// nest's machine defaults for its material and thickness, read from the machine configurations in
+/// (a folder of machine JSON files), applied over the cutting
+/// parameters in (a cutting-parameters JSON file) when that is
+/// set, or over the built-in defaults.
+///
+[Trait("Category", "ProductionCorpus")]
+public class ProductionCorpusSweepTests(ITestOutputHelper output)
+{
+ internal const string CorpusVariable = "OPENNEST_PRODUCTION_CORPUS";
+ internal const string ReportVariable = "OPENNEST_PRODUCTION_CORPUS_REPORT";
+ internal const string MachinesVariable = "OPENNEST_PRODUCTION_CORPUS_MACHINES";
+ internal const string ParametersVariable = "OPENNEST_PRODUCTION_CORPUS_PARAMETERS";
+ internal const string ParallelismVariable = "OPENNEST_PRODUCTION_CORPUS_PARALLELISM";
+ internal const string TimeoutVariable = "OPENNEST_PRODUCTION_CORPUS_TIMEOUT";
+ internal const string FilterVariable = "OPENNEST_PRODUCTION_CORPUS_FILTER";
+ internal const string BudgetVariable = "OPENNEST_PRODUCTION_CORPUS_BUDGET";
+
+ private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
+
+ [SkippableFact]
+ public void ProductionCorpus_PlansEveryPlateAndWritesAReport()
+ {
+ var folder = Environment.GetEnvironmentVariable(CorpusVariable);
+ Skip.If(string.IsNullOrWhiteSpace(folder), $"Set {CorpusVariable} to a folder of .nest files to sweep them.");
+ Assert.True(Directory.Exists(folder), $"{folder} does not exist.");
+ var report = Environment.GetEnvironmentVariable(ReportVariable) is { Length: > 0 } path ? path
+ : Path.Combine(folder!, "sweep.jsonl");
+ var parallelism = int.TryParse(Environment.GetEnvironmentVariable(ParallelismVariable), out var p) && p > 0 ? p : 4;
+ var limit = TimeSpan.FromSeconds(int.TryParse(Environment.GetEnvironmentVariable(TimeoutVariable), out var s) && s > 0 ? s : 180);
+ var multiplier = double.TryParse(Environment.GetEnvironmentVariable(BudgetVariable), out var m) && m > 0 ? m : 1.0;
+ var filters = (Environment.GetEnvironmentVariable(FilterVariable) ?? "")
+ .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ var machines = Environment.GetEnvironmentVariable(MachinesVariable) is { Length: > 0 } dir ? new LocalJsonProvider(dir) : null;
+ var lastParameters = Environment.GetEnvironmentVariable(ParametersVariable) is { Length: > 0 } file
+ ? File.ReadAllText(file) : null;
+
+ var files = Directory.GetFiles(folder!, "*.nest").OrderBy(f => f, StringComparer.Ordinal)
+ .Where(f => filters.Length == 0 || filters.Any(n => Path.GetFileName(f).Contains(n, StringComparison.OrdinalIgnoreCase)))
+ .ToArray();
+ output.WriteLine($"{files.Length} nests, parallelism {parallelism}, limit {limit.TotalSeconds:0} s, budget x{multiplier}, report {report}");
+ var lines = new ConcurrentQueue();
+ using var writer = new StreamWriter(report, append: false);
+ var written = 0;
+ var done = 0;
+ Parallel.ForEach(files, new ParallelOptions { MaxDegreeOfParallelism = parallelism }, file =>
+ {
+ foreach (var row in Sweep(file, machines, lastParameters, limit, multiplier))
+ {
+ var line = JsonSerializer.Serialize(row, JsonOptions);
+ lock (writer)
+ {
+ writer.WriteLine(line);
+ writer.Flush();
+ written++;
+ }
+ }
+ var count = Interlocked.Increment(ref done);
+ if (count % 25 == 0)
+ output.WriteLine($"{count}/{files.Length} nests, {written} plates");
+ });
+ output.WriteLine($"Done: {written} plates from {files.Length} nests in {report}");
+ }
+
+ private static IEnumerable Sweep(string file, LocalJsonProvider machines, string lastParameters,
+ TimeSpan limit, double multiplier)
+ {
+ var id = Path.GetFileNameWithoutExtension(file);
+ Nest nest;
+ try
+ {
+ using var stream = File.OpenRead(file);
+ nest = new NestReader(stream).Read();
+ }
+ catch (Exception e)
+ {
+ return [new PlateRow { Nest = id, Plate = 0, Outcome = "ReadError", Error = e.GetType().Name + ": " + e.Message }];
+ }
+ var machine = nest.Machine == null ? null : machines?.GetMachine(nest.Machine.Id);
+ var rows = new List();
+ for (var index = 0; index < nest.Plates.Count; index++)
+ {
+ var plate = nest.Plates[index];
+ if (plate.Parts.Count == 0 || plate.Parts.All(p => p.BaseDrawing.IsCutOff))
+ continue;
+ var number = index + 1;
+ var row = new PlateRow
+ {
+ Nest = id,
+ Name = nest.Name,
+ Plate = number,
+ Material = $"{nest.Material?.Name} {nest.Material?.Grade} {nest.Thickness}",
+ Machine = nest.Machine?.Name,
+ Units = nest.Units.ToString(),
+ };
+ try
+ {
+ // Clearing forgets the plate's recorded parameters: read them first.
+ var (parameters, source) = ParametersFor(plate, nest, machine, lastParameters);
+ row.ParametersSource = source;
+ row.SupportTabs = parameters.SupportTabs;
+ row.SupportBed = plate.SupportBed != null;
+ // As Plan Cutting after "Clear Lead-ins & Cut-offs": the parts regenerate from clean geometry.
+ var cleared = AppliedCuttingWork.Clear([plate], [number]);
+ row.ClearedLeadIns = cleared.LeadInParts;
+ row.ClearedLockedLeadIns = cleared.LockedLeadInParts;
+ row.ClearedCutOffs = cleared.CutOffs;
+ row.Parts = plate.Parts.Count;
+ row.Pinned = plate.Parts.Count(p => p.HasActiveDrawingPins);
+ row.Budget = CuttingPlanBatch.PlateBudget(plate.Parts.Count, multiplier);
+ var watch = Stopwatch.StartNew();
+ using var cancellation = new CancellationTokenSource(limit);
+ try
+ {
+ var planned = CuttingPlanBatch.Capture([plate], parameters, preservePartOrder: false, [number],
+ cancellation.Token, multiplier).Plan(token: cancellation.Token).Plates[0];
+ row.Seconds = watch.Elapsed.TotalSeconds;
+ Fill(row, planned);
+ }
+ catch (OperationCanceledException)
+ {
+ row.Seconds = watch.Elapsed.TotalSeconds;
+ row.Outcome = "Timeout";
+ }
+ }
+ catch (Exception e)
+ {
+ row.Outcome = "Error";
+ row.Error = e.GetType().Name + ": " + e.Message;
+ }
+ rows.Add(row);
+ }
+ return rows;
+ }
+
+ private static void Fill(PlateRow row, CuttingPlanPlateResult planned)
+ {
+ var result = planned.Result;
+ row.Status = result.Status == CuttingPlanStatus.Canceled ? "Timeout" : result.Status.ToString();
+ row.Outcome = result.Status == CuttingPlanStatus.Canceled ? "Timeout" : ProblemNestRegressionTests.Outcome(planned);
+ row.Replayed = result.IndependentlyReplayed;
+ row.SafestRoute = result.IsSafestRouteFallback;
+ row.HeadRaises = result.HeadRaises;
+ row.KeptOrder = planned.KeptCurrentOrder;
+ row.Expansions = result.Expansions;
+ row.ReorderExpansions = planned.ReorderAttempt?.Expansions;
+ row.OverlapPairs = planned.Overlap?.Pairs.Count;
+ row.OverlapComplete = planned.Overlap?.IsComplete;
+ // Spacing never steers the plan; it is recorded so PEP-converted layouts can be told apart.
+ row.PartSpacing = planned.Plate.PartSpacing;
+ row.SpacingPairs = planned.Overlap?.SpacingPairs.Count;
+ row.NarrowestGap = planned.Overlap?.SpacingPairs.Count > 0 ? planned.Overlap.SpacingPairs.Min(p => p.Gap) : null;
+ row.Warnings = result.Findings.GroupBy(f => f.Kind?.ToString() ?? "None")
+ .ToDictionary(g => g.Key, g => g.Count());
+ row.Findings = result.Findings.Select(f => f.Message).Distinct().Take(12).ToArray();
+ // Which drawings carry the geometry a material or replay check refused, to size such a class by drawing.
+ row.GeometryParts = result.Findings
+ .Where(f => f.Kind is null or PostVerificationKind.Incomplete && f.SourcePart?.BaseDrawing != null
+ && !f.Message.StartsWith("Best-effort", StringComparison.Ordinal))
+ .Select(f => $"{f.SourcePart.BaseDrawing.Name}: {f.Message}").Distinct().Take(8).ToArray();
+ }
+
+ // The plate's own parameters, else the machine's for the nest over the last-applied ones, else those alone.
+ private static (CuttingParameters Parameters, string Source) ParametersFor(Plate plate, Nest nest,
+ MachineConfig machine, string lastParameters)
+ {
+ if (plate.CuttingParameters != null)
+ return (plate.CuttingParameters, "plate");
+ var parameters = lastParameters == null ? new CuttingParameters() : CuttingParametersSerializer.Deserialize(lastParameters);
+ var source = lastParameters == null ? "defaults" : "last";
+ if (MachineCuttingDefaults.ApplyTo(parameters, machine, nest))
+ source = "machine+" + source;
+ return (parameters, source);
+ }
+
+ private sealed class PlateRow
+ {
+ public string Nest { get; set; }
+ public string Name { get; set; }
+ public int Plate { get; set; }
+ public string Material { get; set; }
+ public string Machine { get; set; }
+ public string Units { get; set; }
+ public string ParametersSource { get; set; }
+ public bool SupportTabs { get; set; }
+ public bool SupportBed { get; set; }
+ public int Parts { get; set; }
+ public int Pinned { get; set; }
+ public int ClearedLeadIns { get; set; }
+ public int ClearedLockedLeadIns { get; set; }
+ public int ClearedCutOffs { get; set; }
+ public int Budget { get; set; }
+ public string Outcome { get; set; }
+ public string Status { get; set; }
+ public bool Replayed { get; set; }
+ public bool SafestRoute { get; set; }
+ public int HeadRaises { get; set; }
+ public bool KeptOrder { get; set; }
+ public int Expansions { get; set; }
+ public int? ReorderExpansions { get; set; }
+ public int? OverlapPairs { get; set; }
+ public bool? OverlapComplete { get; set; }
+ public double PartSpacing { get; set; }
+ public int? SpacingPairs { get; set; }
+ public double? NarrowestGap { get; set; }
+ public double Seconds { get; set; }
+ public Dictionary Warnings { get; set; }
+ public string[] Findings { get; set; }
+ public string[] GeometryParts { get; set; }
+ public string Error { get; set; }
+ }
+}
diff --git a/docs/problem-nests.md b/docs/problem-nests.md
index 5b3b3f2f..ebc88762 100644
--- a/docs/problem-nests.md
+++ b/docs/problem-nests.md
@@ -93,3 +93,28 @@ Plates use their own cutting parameters. A plate without them uses line lead-ins
Record the outcome the code produces today, not the outcome you want. A nest that is still
blocked guards against getting worse, and its baseline moves up once a fix lands.
+
+## Sweeping a production archive
+
+`ProductionCorpusSweepTests` (`Category=ProductionCorpus`) is a survey, not a gate: it plans
+every occupied plate of every `.nest` file in a local folder the way Plan Cutting does after
+**Clear Lead-ins & Cut-offs** (the parts regenerate from clean geometry, the plate's own cutting
+parameters when it has them, else the nest's machine defaults over a cutting-parameters file)
+and writes one JSON line per plate: outcome, warnings by kind, safest-route and head-raise
+flags, expansions against the budget, time, and the spacing pairs the overlap label would show.
+It never fails on an outcome. The folder holds production nests, so it stays outside the
+repository, and so does the report.
+
+```sh
+OPENNEST_PRODUCTION_CORPUS= \
+OPENNEST_PRODUCTION_CORPUS_MACHINES= \
+OPENNEST_PRODUCTION_CORPUS_PARAMETERS= \
+OPENNEST_PRODUCTION_CORPUS_REPORT= \
+ dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Release --filter "Category=ProductionCorpus"
+```
+
+`OPENNEST_PRODUCTION_CORPUS_PARALLELISM` (default 4) plans that many nests at once, so times
+and the 180 s `Timeout` (`..._TIMEOUT`) are under load; `..._FILTER` is a comma-separated list
+of file-name fragments and `..._BUDGET` the planning-effort multiplier. Compare two reports plate
+by plate: a plate that ranks lower, or keeps its rank with a safest route or more head raises
+than before, is a regression. Without the folder variable the test skips.