diff --git a/OpenNest.Engine/CuttingPlanning/ContourEntrySelection.cs b/OpenNest.Engine/CuttingPlanning/ContourEntrySelection.cs
new file mode 100644
index 0000000..a1964bb
--- /dev/null
+++ b/OpenNest.Engine/CuttingPlanning/ContourEntrySelection.cs
@@ -0,0 +1,250 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using OpenNest.CNC.CuttingPlanning;
+using OpenNest.Diagnostics;
+using OpenNest.Geometry;
+
+namespace OpenNest.Engine.CuttingPlanning;
+
+/// Why a selection ended the way it did. Honest metadata, never a geometric overclaim.
+internal enum ContourSelectionShortfall
+{
+ /// Selection ended at the cap with the side scan satisfied: a complete verdict.
+ None,
+
+ ///
+ /// The finite catalogue was fully evaluated and the selection is all there is. With an
+ /// empty selection this is exactly the sentence "no tested lead-in fits on part N,
+ /// contour M" — and ONLY here may that sentence be spoken.
+ ///
+ Exhausted,
+
+ ///
+ /// At least one check could not complete (incomplete material or emission). Never
+ /// present this as geometric impossibility and never as "nothing fits".
+ ///
+ Incomplete,
+}
+
+///
+/// Outcome of one bounded selection: selected choices in global rank order, how many
+/// distinct candidates were evaluated, which ones (in catalogue order, for cost tests), and
+/// why the selection stopped.
+///
+internal sealed record ContourSelectionResult(
+ IReadOnlyList Choices,
+ int EvaluatedCount,
+ IReadOnlyList<(long, long)> EvaluatedKeys,
+ ContourSelectionShortfall Shortfall,
+ string? Reason);
+
+///
+/// Bounded, lazy selection of lead-feasible entry candidates for ONE contour. Greedy global
+/// rank order until the cap (default 16) fills or the finite catalogue ends — rejected and
+/// incomplete candidates never consume a slot. When the cap can afford it (5+), one
+/// corrective scan makes sure every side of the candidate bounding rectangle that HAS a
+/// feasible candidate is represented, replacing the worst selected candidate only when
+/// every other covered side survives; one corner may cover two sides. The cap is never
+/// exceeded and points are never manufactured on infeasible sides — a capped selection may
+/// truthfully omit a feasible side. Sides are exactly the ranker's side geometry (the
+/// candidate-set bounding rectangle). Verdicts are memoized per attempt (the adapter
+/// memoizes too): no candidate is ever evaluated twice, and once selection settles the
+/// untouched tail is never evaluated. No search/DFS changes and no larger search budget.
+///
+internal static class ContourEntrySelection
+{
+ internal const int DefaultMaxEntries = 16;
+
+ /// Below this cap all-side coverage is not promised; the cap and ranking bind first (caps 1-3).
+ internal const int SideCoverageMinCap = 4;
+
+ ///
+ /// Selects up to feasible choices from one contour's
+ /// globally ranked catalogue. is the S07 adapter verdict,
+ /// called lazily at most once per distinct candidate.
+ ///
+ internal static ContourSelectionResult Select(
+ IReadOnlyList rankedCandidates,
+ Func evaluate,
+ int maxEntries = DefaultMaxEntries,
+ CancellationToken token = default)
+ {
+ token.ThrowIfCancellationRequested();
+ if (rankedCandidates == null)
+ throw new ArgumentException("Ranked candidates are required.", nameof(rankedCandidates));
+ if (evaluate == null)
+ throw new ArgumentException("A feasibility evaluation is required.", nameof(evaluate));
+ if (maxEntries <= 0)
+ throw new ArgumentException("The cap must be positive.", nameof(maxEntries));
+
+ // Distinct points only — the cap must not double-count a geometric duplicate.
+ var candidates = new List();
+ var seen = new HashSet<(long, long)>();
+ foreach (var candidate in rankedCandidates)
+ if (seen.Add(candidate.GeometryKey))
+ candidates.Add(candidate);
+
+ var evaluated = new List();
+ var verdicts = new Dictionary<(long, long), ContourFeasibilityVerdict>();
+ var box = Box(candidates);
+
+ var selected = new List();
+ var sawIncomplete = false;
+ var index = 0;
+
+ ContourFeasibilityVerdict Verdict(ContourEntryCandidate candidate)
+ {
+ if (!verdicts.TryGetValue(candidate.GeometryKey, out var known))
+ {
+ token.ThrowIfCancellationRequested();
+ known = evaluate(candidate);
+ verdicts[candidate.GeometryKey] = known;
+ evaluated.Add(candidate);
+ }
+ return known;
+ }
+
+ // Phase 1: greedy global order until the cap fills or the catalogue ends.
+ for (; index < candidates.Count && selected.Count < maxEntries; index++)
+ {
+ var verdict = Verdict(candidates[index]);
+ if (verdict.Status == ContourFeasibilityStatus.Incomplete)
+ {
+ sawIncomplete = true;
+ break;
+ }
+ if (verdict.IsClear)
+ selected.Add(candidates[index]);
+ }
+
+ // Phase 2: side coverage when the cap affords it. Every missing side with an
+ // unexamined tail is chased lazily; a clear candidate on that side is appended when
+ // a slot remains, otherwise it replaces the worst selected candidate whose removal
+ // keeps every other covered side covered.
+ var coverage = maxEntries >= SideCoverageMinCap && selected.Count > 0;
+ if (coverage && !sawIncomplete)
+ for (var side = 0; side < 4 && !sawIncomplete; side++)
+ {
+ if (selected.Any(c => Sides(c, box).Contains(side)))
+ continue;
+ for (; index < candidates.Count; index++)
+ {
+ var verdict = Verdict(candidates[index]);
+ if (verdict.Status == ContourFeasibilityStatus.Incomplete)
+ {
+ sawIncomplete = true;
+ break;
+ }
+ if (!verdict.IsClear || !Sides(candidates[index], box).Contains(side))
+ continue;
+ TryPlace(selected, candidates[index], box, maxEntries);
+ break;
+ }
+ }
+
+ var exhausted = index >= candidates.Count;
+ ContourSelectionShortfall shortfall;
+ string? reason;
+ if (sawIncomplete)
+ {
+ shortfall = ContourSelectionShortfall.Incomplete;
+ reason = "At least one lead check could not complete; this is not a geometric verdict and nothing is proven impossible.";
+ }
+ else if (selected.Count >= maxEntries)
+ {
+ shortfall = ContourSelectionShortfall.None;
+ reason = null;
+ }
+ else if (exhausted)
+ {
+ shortfall = ContourSelectionShortfall.Exhausted;
+ reason = selected.Count == 0
+ ? "No tested lead-in fits on this contour."
+ : null;
+ }
+ else
+ {
+ // Unreachable: phase 1 ends at cap or catalogue end and phase 2 chases every
+ // side to the end; a cancellation throws before this point.
+ shortfall = ContourSelectionShortfall.Incomplete;
+ reason = "Selection stopped before the catalogue ended.";
+ }
+
+ var ordered = selected
+ .OrderBy(c => evaluated.FindIndex(x => x.GeometryKey == c.GeometryKey) is var e && e >= 0
+ ? e : candidates.FindIndex(x => x.GeometryKey == c.GeometryKey))
+ .Select(c => c.Choice)
+ .ToList();
+ return new(ordered, evaluated.Count,
+ evaluated.Select(c => c.GeometryKey).ToList(), shortfall, reason);
+ }
+
+ private static (double MinX, double MinY, double MaxX, double MaxY) Box(
+ List candidates)
+ {
+ double minX = double.PositiveInfinity, minY = double.PositiveInfinity,
+ maxX = double.NegativeInfinity, maxY = double.NegativeInfinity;
+ foreach (var c in candidates)
+ {
+ var p = c.Choice.Point;
+ if (p.X < minX) minX = p.X;
+ if (p.X > maxX) maxX = p.X;
+ if (p.Y < minY) minY = p.Y;
+ if (p.Y > maxY) maxY = p.Y;
+ }
+ if (!double.IsFinite(minX))
+ return (0, 0, 0, 0);
+ return (minX, minY, maxX, maxY);
+ }
+
+ /// Sides 0 left, 1 right, 2 bottom, 3 top of the candidate bounding rectangle that the point sits on.
+ private static List Sides(ContourEntryCandidate candidate,
+ (double MinX, double MinY, double MaxX, double MaxY) box)
+ {
+ var p = candidate.Choice.Point;
+ var left = p.X - box.MinX;
+ var right = box.MaxX - p.X;
+ var bottom = p.Y - box.MinY;
+ var top = box.MaxY - p.Y;
+ var min = System.Math.Min(System.Math.Min(left, right), System.Math.Min(bottom, top));
+ var sides = new List(2);
+ if (left <= min + PostVerificationGeometry.Epsilon) sides.Add(0);
+ if (right <= min + PostVerificationGeometry.Epsilon) sides.Add(1);
+ if (bottom <= min + PostVerificationGeometry.Epsilon) sides.Add(2);
+ if (top <= min + PostVerificationGeometry.Epsilon) sides.Add(3);
+ return sides;
+ }
+
+ /// Appends when a slot remains, else replaces the worst (latest-ranked) candidate whose removal preserves every other covered side.
+ private static void TryPlace(List selected, ContourEntryCandidate candidate,
+ (double MinX, double MinY, double MaxX, double MaxY) box, int maxEntries)
+ {
+ if (selected.Count < maxEntries)
+ {
+ selected.Add(candidate);
+ return;
+ }
+ var incoming = Sides(candidate, box);
+ for (var i = selected.Count - 1; i >= 0; i--)
+ {
+ var removalSafe = true;
+ for (var side = 0; side < 4 && removalSafe; side++)
+ {
+ if (incoming.Contains(side))
+ continue; // the replacement covers it
+ var covers = selected.Where((c, at) => at != i && Sides(c, box).Contains(side)).Any();
+ var wasCovered = selected.Any(c => Sides(c, box).Contains(side));
+ if (wasCovered && !covers)
+ removalSafe = false;
+ }
+ if (removalSafe)
+ {
+ selected[i] = candidate;
+ return;
+ }
+ }
+ // No safe victim: the side stays truthfully unrepresented at this cap.
+ }
+}
diff --git a/OpenNest.Tests/CuttingPlanning/ContourEntrySelectionTests.cs b/OpenNest.Tests/CuttingPlanning/ContourEntrySelectionTests.cs
new file mode 100644
index 0000000..d3e0577
--- /dev/null
+++ b/OpenNest.Tests/CuttingPlanning/ContourEntrySelectionTests.cs
@@ -0,0 +1,268 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using OpenNest.CNC;
+using OpenNest.CNC.CuttingPlanning;
+using OpenNest.Engine.CuttingPlanning;
+using OpenNest.Geometry;
+
+namespace OpenNest.Tests.CuttingPlanning;
+
+///
+/// The bounded, lazy selection over ranked candidates and the S07 verdict: cap first, side
+/// coverage as a corrective scan, honest shortfall metadata — and the untouched tail is
+/// never evaluated. Counts, not elapsed time.
+///
+public class ContourEntrySelectionTests
+{
+ private static readonly PreparedContours Owner =
+ PreparedContours.Capture(ExplicitContourTests.Square(false), ExplicitContourTests.Parameters());
+
+ private static ContourEntryCandidate Cand(double x, double y, AutomaticEntryKind kind = AutomaticEntryKind.ConvexCorner) =>
+ new(new ContourChoice(0, 0, new Vector(x, y)) { Owner = Owner }, kind);
+
+ private static Func Feasible(params (double X, double Y)[] clearAt)
+ {
+ var set = new HashSet<(long, long)>(clearAt.Select(p => Cand(p.X, p.Y).GeometryKey));
+ return c => set.Contains(c.GeometryKey)
+ ? new(ContourFeasibilityStatus.Clear, null)
+ : new(ContourFeasibilityStatus.Blocked, "test block");
+ }
+
+ private static Func AllClear() =>
+ _ => new(ContourFeasibilityStatus.Clear, null);
+
+ private static bool Selected(ContourSelectionResult result, double x, double y) =>
+ result.Choices.Any(c => c.Point.DistanceTo(new Vector(x, y)) < 1e-9);
+
+ [Fact]
+ public void LaterSafeCandidateSurvivesTwentyRejectedRivals()
+ {
+ // 18 blocked facing candidates cannot crowd out the one safe point at the tail.
+ var candidates = new List();
+ for (var i = 0; i < 18; i++)
+ candidates.Add(Cand(i * 0.001, 0));
+ candidates.Add(Cand(100, 100));
+
+ var result = ContourEntrySelection.Select(candidates, Feasible((100, 100)));
+
+ Assert.Single(result.Choices);
+ Assert.True(Selected(result, 100, 100));
+ Assert.Equal(19, result.EvaluatedCount);
+ Assert.Equal(ContourSelectionShortfall.Exhausted, result.Shortfall);
+ }
+
+ [Fact]
+ public void EveryFeasibleSideIsRepresented()
+ {
+ // Corners on left/bottom and right/bottom, plus one point each on top-left and
+ // top-right; a wide tail of blocked points so coverage must chase the sides lazily.
+ var candidates = new List
+ {
+ Cand(0, 0), Cand(10, 0), // bottom corners
+ Cand(0, 10, AutomaticEntryKind.StraightMidpoint), // top-left
+ Cand(10, 10, AutomaticEntryKind.TangentJoint), // top-right
+ };
+ for (var i = 1; i < 30; i++)
+ candidates.Add(Cand(i * 0.3, 0, AutomaticEntryKind.NearCorner)); // blocked bottom tail
+
+ var result = ContourEntrySelection.Select(candidates,
+ Feasible((0, 0), (10, 0), (0, 10), (10, 10)), maxEntries: 6);
+
+ Assert.Equal(4, result.Choices.Count);
+ Assert.True(Selected(result, 0, 10));
+ Assert.True(Selected(result, 10, 10));
+ // The blocked tail was chased to its end exactly once per missing side scan.
+ Assert.Equal(ContourSelectionShortfall.Exhausted, result.Shortfall);
+ }
+
+ [Fact]
+ public void BlockedSideIsOmittedWithoutManufacturingPoints()
+ {
+ // No feasible candidate touches the top side.
+ var candidates = new List
+ {
+ Cand(0, 0), Cand(10, 0), Cand(0, 10), Cand(10, 10),
+ };
+
+ var result = ContourEntrySelection.Select(candidates, Feasible((0, 0), (10, 0)));
+
+ Assert.Equal(2, result.Choices.Count);
+ Assert.False(Selected(result, 0, 10));
+ Assert.False(Selected(result, 10, 10));
+ // Every selected choice was feasible: nothing was manufactured.
+ Assert.All(result.Choices, c => Assert.InRange(c.Point.Y, 0, 0));
+ }
+
+ [Fact]
+ public void CapIsNeverExceededAndRankingComesFirst()
+ {
+ var candidates = new List();
+ for (var i = 0; i < 10; i++)
+ candidates.Add(Cand(10 - i * 0.01, 0)); // first-ranked points on the right/bottom
+
+ var result = ContourEntrySelection.Select(candidates, AllClear(), maxEntries: 3);
+
+ Assert.Equal(3, result.Choices.Count);
+ Assert.Equal(3, result.EvaluatedCount); // stopped the moment the cap filled
+ Assert.Equal(ContourSelectionShortfall.None, result.Shortfall);
+ Assert.Equal(10.0, result.Choices[0].Point.X, 6); // rank-first point kept
+ }
+
+ [Fact]
+ public void NormalCaseNeverEvaluatesTheUntouchedTail()
+ {
+ // Four corners first (all sides covered), then a long clear tail. The cap fills at
+ // 16 with full coverage, so the chase runs empty and the tail stays untouched.
+ var candidates = new List
+ {
+ Cand(0, 0), Cand(10, 0), Cand(0, 10), Cand(10, 10),
+ };
+ for (var i = 0; i < 36; i++)
+ candidates.Add(Cand(1 + i * 0.1, 1));
+
+ var result = ContourEntrySelection.Select(candidates, AllClear(), maxEntries: 16);
+
+ Assert.Equal(16, result.Choices.Count);
+ Assert.Equal(16, result.EvaluatedCount);
+ Assert.DoesNotContain(candidates[16].GeometryKey, result.EvaluatedKeys);
+ Assert.DoesNotContain(candidates[^1].GeometryKey, result.EvaluatedKeys);
+ Assert.Equal(ContourSelectionShortfall.None, result.Shortfall);
+ }
+
+ [Fact]
+ public void AllBlockedIsExhaustedGeometricImpossibility()
+ {
+ var candidates = new[] { Cand(0, 0), Cand(10, 0), Cand(0, 10), Cand(10, 10) };
+
+ var result = ContourEntrySelection.Select(candidates,
+ _ => new(ContourFeasibilityStatus.Blocked, "test block"));
+
+ Assert.Empty(result.Choices);
+ Assert.Equal(4, result.EvaluatedCount);
+ Assert.Equal(ContourSelectionShortfall.Exhausted, result.Shortfall);
+ Assert.Contains("No tested lead-in fits", result.Reason);
+ }
+
+ [Fact]
+ public void IncompleteCheckIsNeverGeometricImpossibility()
+ {
+ var candidates = new[] { Cand(0, 0), Cand(10, 0) };
+ var first = true;
+
+ var result = ContourEntrySelection.Select(candidates, _ =>
+ {
+ ContourFeasibilityVerdict verdict = first
+ ? new(ContourFeasibilityStatus.Incomplete, "incomplete material")
+ : new(ContourFeasibilityStatus.Clear, null);
+ first = false;
+ return verdict;
+ });
+
+ Assert.Equal(ContourSelectionShortfall.Incomplete, result.Shortfall);
+ Assert.DoesNotContain("No tested lead-in fits", result.Reason ?? string.Empty);
+ Assert.Contains("not a geometric verdict", result.Reason);
+ }
+
+ [Fact]
+ public void SmallCapsObeyTheCapNotCoverage()
+ {
+ var candidates = new List
+ {
+ Cand(0, 0), Cand(10, 0), Cand(0, 10), Cand(10, 10),
+ };
+
+ var cap1 = ContourEntrySelection.Select(candidates, AllClear(), maxEntries: 1);
+ var cap3 = ContourEntrySelection.Select(candidates, AllClear(), maxEntries: 3);
+
+ Assert.Single(cap1.Choices);
+ Assert.Equal(3, cap3.Choices.Count);
+ Assert.Equal(1, cap1.EvaluatedCount); // cap fills before any coverage scan
+ Assert.Equal(3, cap3.EvaluatedCount);
+ }
+
+ [Fact]
+ public void InvalidCapStaysInvalid()
+ {
+ Assert.Throws(() =>
+ ContourEntrySelection.Select(Array.Empty(), _ => null!, 0));
+ Assert.Throws(() =>
+ ContourEntrySelection.Select(Array.Empty(), _ => null!, -1));
+ }
+
+ [Fact]
+ public void DuplicatePointsNeverConsumeTwoSlots()
+ {
+ var candidates = new[] { Cand(5, 5), Cand(5, 5), Cand(5, 5) };
+
+ var result = ContourEntrySelection.Select(candidates, AllClear(), maxEntries: 2);
+
+ Assert.Single(result.Choices);
+ Assert.Equal(1, result.EvaluatedCount); // deduplicated before evaluation
+ }
+
+ [Fact]
+ public void EmptyCatalogueIsExhaustedWithoutEvaluation()
+ {
+ var result = ContourEntrySelection.Select(Array.Empty(), _ => null!);
+
+ Assert.Empty(result.Choices);
+ Assert.Equal(0, result.EvaluatedCount);
+ Assert.Equal(ContourSelectionShortfall.Exhausted, result.Shortfall);
+ }
+
+ [Fact]
+ public void CancellationStopsBeforeAnyEvaluation()
+ {
+ using var cancelled = new CancellationTokenSource();
+ cancelled.Cancel();
+ var candidates = new[] { Cand(0, 0) };
+
+ Assert.ThrowsAny(() =>
+ ContourEntrySelection.Select(candidates, _ => new(ContourFeasibilityStatus.Clear, null),
+ token: cancelled.Token));
+ }
+
+ [Fact]
+ public void CoverageReplacementDropsTheWorstAndResorts()
+ {
+ // Cap 5 fills on points covering left/bottom/top; the right side is chased to (6,6),
+ // which must displace the worst selected point whose removal preserves the others
+ // ((3,4), the top-only cover is re-covered by (6,6)) and settle at its true rank slot.
+ var candidates = new List
+ {
+ Cand(0, 0), Cand(2, 0), Cand(0, 2), Cand(0, 4), Cand(3, 4), Cand(6, 6),
+ };
+
+ var result = ContourEntrySelection.Select(candidates, AllClear(), maxEntries: 5);
+
+ Assert.Equal(5, result.Choices.Count);
+ Assert.True(Selected(result, 6, 6));
+ Assert.False(Selected(result, 3, 4));
+ Assert.Equal(6, result.EvaluatedCount);
+ // Evaluation order IS global rank order; the late chaser sorts to the end.
+ Assert.Equal(6, result.Choices[^1].Point.X);
+ Assert.Equal(0, result.Choices[0].Point.X);
+ Assert.Equal(0, result.Choices[0].Point.Y);
+ }
+
+ [Fact]
+ public void CoverageNeverDestroysTheOnlyCoverOfAnotherSide()
+ {
+ // (2,0) is the ONLY cover of bottom; chasing the right side must not remove it even
+ // though it outranks nothing. The chaser replaces (2,4) (top, re-covered by (6,6)).
+ var candidates = new List
+ {
+ Cand(0, 0), Cand(2, 0), Cand(0, 2), Cand(2, 4), Cand(6, 6),
+ };
+
+ var result = ContourEntrySelection.Select(candidates, AllClear(), maxEntries: 4);
+
+ // Cap 4: phase 1 keeps the first four (right not covered: (2,4) left+top only).
+ Assert.True(Selected(result, 2, 0)); // sole bottom cover survives
+ Assert.True(Selected(result, 6, 6)); // right-side chaser placed
+ Assert.False(Selected(result, 2, 4)); // safely displaced (top re-covered)
+ Assert.Equal(ContourSelectionShortfall.None, result.Shortfall);
+ }
+}