diff --git a/README.md b/README.md
index 276d64d..0fcd062 100644
--- a/README.md
+++ b/README.md
@@ -120,6 +120,20 @@ var domainResult = NestResultMaterializer.Materialize(job, result);
**Legacy caller boundaries (not yet migrated):** the desktop UI (`MainForm.RunAutoNestAsync` / `NestSinglePlateAsync`), the CLI (`OpenNest.Console`), and MCP (`NestingTools`) still call the old single-plate `engine.Nest(...)` entry points unchanged. UI adoption needs a separate adapter preserving populated-plate editing, preview routing, and Accept-versus-Cancel semantics. The public API (`OpenNest.Api`, `NestRunner.RunAsync`) already delegates to one `NestJobRunner.Solve` call and reports status, stop reason, part fulfillment, stock usage, and plate-to-stock mapping; `.nestquote` archives carry a schema version and round-trip incomplete jobs.
+### Fresh DXF job verification (headless)
+
+`tools/NestDxfJob` imports a complete quantity workbook and runs a registered whole-job engine. It never reuses saved drawing geometry or placements. The workbook must have a `Parts` worksheet with exactly one `Part Name` and `Qty Required` column; names match DXF filename stems exactly. Invalid/fractional/negative quantities, duplicate names, and missing required DXFs fail explicitly. Zero-demand rows are not imported; additional DXFs with no positive workbook demand are listed and not assigned an invented quantity.
+
+```bash
+dotnet run --project tools/NestDxfJob -- \
+ /path/to/dxfs /path/to/parts.xlsx /path/to/settings.nest \
+ /path/to/new-results-directory Strip
+```
+
+The settings nest supplies units, material metadata, per-part rotation constraints/priority where names match, and distinct plate dimensions/clearances/quadrants. Stock is unlimited copies of those settings with a 40-sheet cap, not a claim about physical inventory. DXFs with explicit conflicting units reject; unitless DXFs use the template units without rescaling. `CadImporter` is called with `DetectBends = false`: default DXF filtering removes case-insensitive `ETCH`/`SCRIBE` layers before optimization, and bend detection cannot regenerate marks.
+
+The output directory must not exist. The tool writes `imported-cut-only.nest` and `import-report.json` (including input hashes, excluded marks, and unmatched DXFs), then runs the selected engine with a ten-minute cancellation budget. A complete result must pass quantity, bounds, overlap/spacing and cut-only checks, then save/reload and pass them again before success. `validation-report.json` records per-part fulfillment and placements. A partial or invalid result exits nonzero and is not published as a successful nest. Use `import-only` instead of an engine name to verify and save only the imported job. This verifies nesting geometry, not machine-ready CNC lead-ins or post-processing.
+
### Run
```bash
diff --git a/tools/NestDxfJob/NestDxfJob.csproj b/tools/NestDxfJob/NestDxfJob.csproj
new file mode 100644
index 0000000..9418a95
--- /dev/null
+++ b/tools/NestDxfJob/NestDxfJob.csproj
@@ -0,0 +1,10 @@
+
+
+ Exe
+ net8.0
+ enable
+
+
+
+
+
diff --git a/tools/NestDxfJob/Program.cs b/tools/NestDxfJob/Program.cs
new file mode 100644
index 0000000..93160aa
--- /dev/null
+++ b/tools/NestDxfJob/Program.cs
@@ -0,0 +1,296 @@
+using System.Diagnostics;
+using System.Globalization;
+using System.Security.Cryptography;
+using System.Text.Json;
+using OpenNest;
+using OpenNest.Benchmark;
+using OpenNest.CNC;
+using OpenNest.IO;
+using OpenNest.IO.Bom;
+
+CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
+if (args.Length != 5)
+{
+ Console.Error.WriteLine(
+ "Usage: NestDxfJob "
+ );
+ return 1;
+}
+
+try
+{
+ var directory = Path.GetFullPath(args[0]);
+ var workbookPath = Path.GetFullPath(args[1]);
+ var templatePath = Path.GetFullPath(args[2]);
+ var outputDirectory = Path.GetFullPath(args[3]);
+ if (Directory.Exists(outputDirectory) || File.Exists(outputDirectory))
+ throw new IOException(
+ "Output directory must be new; existing inputs/results are never overwritten."
+ );
+ var engineInfo =
+ args[4] == "import-only"
+ ? null
+ : NestingEngineRegistry.AvailableEngines.SingleOrDefault(e => e.Name == args[4])
+ ?? throw new ArgumentException($"Unknown engine: {args[4]}");
+ var quantities = PartQuantityReader.Read(workbookPath);
+ var paths = Directory
+ .GetFiles(directory)
+ .Where(p => Path.GetExtension(p).Equals(".dxf", StringComparison.OrdinalIgnoreCase))
+ .Order(StringComparer.Ordinal)
+ .ToArray();
+ var files = paths.ToDictionary(Path.GetFileNameWithoutExtension, StringComparer.Ordinal);
+ var demand = quantities
+ .Where(p => p.Value > 0)
+ .ToDictionary(p => p.Key, p => p.Value, StringComparer.Ordinal);
+ var missing = demand.Keys.Except(files.Keys, StringComparer.Ordinal).ToArray();
+ if (missing.Length > 0)
+ throw new InvalidDataException($"Missing exact-name DXFs: {string.Join(", ", missing)}");
+ var unrequested = files.Keys.Except(demand.Keys, StringComparer.Ordinal).ToArray();
+ foreach (var name in unrequested)
+ Console.WriteLine($"Not requested by workbook (not imported): {files[name]}");
+ var hashes = paths
+ .Append(workbookPath)
+ .Append(templatePath)
+ .Distinct()
+ .ToDictionary(p => p, Hash);
+ var template = new NestReader(templatePath).Read();
+ // Saved placements and drawing geometry are NOT imported from the template.
+ var input = new Nest(template.Name)
+ {
+ Units = template.Units,
+ Material = template.Material,
+ Thickness = template.Thickness,
+ Customer = template.Customer,
+ DateCreated = DateTime.Now,
+ Notes =
+ "Fresh DXF import; ETCH/SCRIBE excluded and bend generation disabled. Quantities from workbook. Template supplies stock settings only.",
+ };
+ var stocks =
+ template.Plates.Count > 0
+ ? template.Plates.ToList()
+ : new List { template.PlateDefaults.CreateNew() };
+ var distinctStocks = stocks
+ .GroupBy(p => new
+ {
+ p.Size,
+ p.Quadrant,
+ p.PartSpacing,
+ p.EdgeSpacing.Left,
+ p.EdgeSpacing.Right,
+ p.EdgeSpacing.Top,
+ p.EdgeSpacing.Bottom,
+ })
+ .Select(g => g.First())
+ .ToList();
+ foreach (var stock in distinctStocks)
+ {
+ input.PlateDefaults.SetFromExisting(stock);
+ input.Plates.Add(input.PlateDefaults.CreateNew());
+ }
+ input.PlateDefaults.SetFromExisting(distinctStocks[0]);
+ var imports = new List