diff --git a/OpenNest.Engine/CuttingPlanning/ContourEntryFeasibility.cs b/OpenNest.Engine/CuttingPlanning/ContourEntryFeasibility.cs
new file mode 100644
index 0000000..6c192fb
--- /dev/null
+++ b/OpenNest.Engine/CuttingPlanning/ContourEntryFeasibility.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using OpenNest.CNC.CuttingPlanning;
+using OpenNest.Geometry;
+
+namespace OpenNest.Engine.CuttingPlanning;
+
+/// The three verdicts a candidate entry can receive from the shared lead validator.
+public enum ContourFeasibilityStatus
+{
+ /// Every emitted lead of this contour is complete AND clear.
+ Clear,
+
+ /// A complete check found a lead contact/overlap — the reason is preserved.
+ Blocked,
+
+ /// The check could not complete (incomplete material, malformed emission) — never clear.
+ Incomplete,
+}
+
+/// Verdict for one candidate entry. Complete/blocked/incomplete stay distinct.
+public sealed record ContourFeasibilityVerdict(ContourFeasibilityStatus Status, string? Reason)
+{
+ public bool IsClear => Status == ContourFeasibilityStatus.Clear;
+}
+
+///
+/// Feasibility adapter over the EXISTING : emits one owned
+/// candidate contour through the S06 diagnostic seam, reads it at the placement position,
+/// and certifies the emitted lead-in and lead-out against ALL placed material. It is not a
+/// new collision implementation and not a plan approval: a Clear verdict certifies this
+/// contour's emitted leads only — the missing-lead (NoLeadIn) check still runs later on the
+/// complete plan, and rapids/pierce clearance belong to their existing checkers. A
+/// candidate rejected here is not proven infeasible by anything else: this adapter only
+/// reports what the validator reported.
+/// One instance is one captured planning attempt: verdicts cache per exact choice and node
+/// context for the instance's lifetime — settings and placement are fixed per instance —
+/// and nothing is cached across instances or statically. Evaluation is lazy: only the
+/// choice handed to is ever emitted or validated, and
+/// counts validator executions (not cache hits) for cost tests.
+///
+public sealed class ContourEntryFeasibility
+{
+ private readonly PreparedContours prepared;
+ private readonly Vector location;
+ private readonly LeadMaterialSnapshot ownMaterial;
+ private readonly LeadMaterialSnapshot[] materials;
+ private readonly Dictionary cache = new();
+
+ public ContourEntryFeasibility(PreparedContours prepared, Vector location,
+ LeadMaterialSnapshot ownMaterial, IReadOnlyList otherMaterials)
+ {
+ this.prepared = prepared ?? throw new ArgumentException("Prepared contours are required.", nameof(prepared));
+ this.ownMaterial = ownMaterial ?? throw new ArgumentException("Own material snapshot is required.", nameof(ownMaterial));
+ if (otherMaterials == null)
+ throw new ArgumentException("Placed-material snapshots are required.", nameof(otherMaterials));
+ this.location = location;
+ // Immutable copy; the validator's own-material-first convention is preserved.
+ var all = new List { this.ownMaterial };
+ foreach (var material in otherMaterials)
+ {
+ if (material == null)
+ throw new ArgumentException("Placed-material snapshots must not be null.", nameof(otherMaterials));
+ if (!ReferenceEquals(material, this.ownMaterial))
+ all.Add(material);
+ }
+ materials = all.ToArray();
+ }
+
+ /// Validator executions performed (cache misses only) during this attempt.
+ public int EvaluationCount { get; private set; }
+
+ ///
+ /// The verdict for one owned candidate. Same choice + node context within this attempt
+ /// is answered from cache. Cancellation propagates; a malformed emission is an
+ /// Incomplete verdict with the emission's own reason, never a crash and never Clear.
+ ///
+ public ContourFeasibilityVerdict Check(ContourChoice choice, string nodeContext = "",
+ CancellationToken token = default)
+ {
+ token.ThrowIfCancellationRequested();
+ if (choice == null)
+ throw new ArgumentException("A contour choice is required.", nameof(choice));
+ var key = new Key(choice.ContourOrdinal, choice.EntityOrdinal,
+ choice.Point.X, choice.Point.Y, nodeContext ?? string.Empty);
+ if (cache.TryGetValue(key, out var known))
+ return known;
+ var verdict = Probe(choice, token);
+ cache[key] = verdict;
+ EvaluationCount++;
+ return verdict;
+ }
+
+ private ContourFeasibilityVerdict Probe(ContourChoice choice, CancellationToken token)
+ {
+ try
+ {
+ var program = prepared.EmitCandidateForValidation(choice);
+ var execution = ExecutionMotionReader.Read(program, location, null, token);
+ var result = LeadPathValidator.Check(execution, ownMaterial, materials, token);
+ if (!result.IsComplete)
+ return new(ContourFeasibilityStatus.Incomplete, result.Reason);
+ return result.IsClear
+ ? new(ContourFeasibilityStatus.Clear, null)
+ : new(ContourFeasibilityStatus.Blocked, result.Reason);
+ }
+ catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
+ {
+ // Malformed emission or foreign choice: refused, reason preserved, never cached as clear.
+ return new(ContourFeasibilityStatus.Incomplete, ex.Message);
+ }
+ }
+
+ private readonly record struct Key(int ContourOrdinal, int EntityOrdinal, double X, double Y, string NodeContext);
+}
diff --git a/OpenNest.Tests/CuttingPlanning/ContourEntryFeasibilityTests.cs b/OpenNest.Tests/CuttingPlanning/ContourEntryFeasibilityTests.cs
new file mode 100644
index 0000000..0b7ed95
--- /dev/null
+++ b/OpenNest.Tests/CuttingPlanning/ContourEntryFeasibilityTests.cs
@@ -0,0 +1,213 @@
+using System.Threading;
+using OpenNest.CNC;
+using OpenNest.CNC.CuttingPlanning;
+using OpenNest.CNC.CuttingStrategy;
+using OpenNest.Engine.CuttingPlanning;
+using OpenNest.Geometry;
+
+namespace OpenNest.Tests.CuttingPlanning;
+
+///
+/// S07: the feasibility adapter certifies EXACTLY what LeadPathValidator certifies for one
+/// emitted candidate contour — no more (NoLeadIn is not a plan approval) and no less
+/// (incomplete checks never read as clear), cached per captured attempt only.
+///
+public class ContourEntryFeasibilityTests
+{
+ private static readonly Vector At = Vector.Zero;
+
+ private static (PreparedContours Prepared, ContourChoice Choice, LeadMaterialSnapshot Own)
+ PreparedSquare(string style = "line")
+ {
+ var clean = ExplicitContourTests.Square(false);
+ var prepared = PreparedContours.Capture(clean, ExplicitContourTests.Parameters(style));
+ var choice = prepared.Entry(0, 0, new Vector(0, 5)); // left edge, mid-side
+ return (prepared, choice, LeadMaterialSnapshot.Capture(clean, At));
+ }
+
+ private static LeadMaterialSnapshot Box(double x1, double y1, double x2, double y2)
+ {
+ var p = new Program();
+ p.MoveTo(x1, y1);
+ p.LineTo(x2, y1); p.LineTo(x2, y2); p.LineTo(x1, y2); p.LineTo(x1, y1);
+ var snapshot = LeadMaterialSnapshot.Capture(p, Vector.Zero);
+ Assert.True(snapshot.IsComplete, snapshot.Reason);
+ return snapshot;
+ }
+
+ [Fact]
+ public void FittingStraightLeadIsClear()
+ {
+ var (prepared, choice, own) = PreparedSquare();
+ var feasibility = new ContourEntryFeasibility(prepared, At, own, []);
+
+ var verdict = feasibility.Check(choice);
+
+ Assert.True(verdict.IsClear, verdict.Reason);
+ Assert.Equal(ContourFeasibilityStatus.Clear, verdict.Status);
+ Assert.Equal(1, feasibility.EvaluationCount);
+ }
+
+ [Fact]
+ public void ArcLeadCandidateVerdictsComeFromTheSharedValidator()
+ {
+ var (prepared, choice, own) = PreparedSquare("arc");
+ var feasibility = new ContourEntryFeasibility(prepared, At, own, []);
+ Assert.True(feasibility.Check(choice).IsClear);
+ }
+
+ [Fact]
+ public void NeighbourContactRejectsKeepingReason()
+ {
+ var (prepared, choice, own) = PreparedSquare();
+ // A neighbouring sheet covering the lead-in approach region.
+ var neighbour = Box(-1.5, 4.4, -0.05, 5.6);
+ var feasibility = new ContourEntryFeasibility(prepared, At, own, [neighbour]);
+
+ var verdict = feasibility.Check(choice);
+
+ Assert.Equal(ContourFeasibilityStatus.Blocked, verdict.Status);
+ Assert.False(verdict.IsClear);
+ Assert.False(string.IsNullOrEmpty(verdict.Reason));
+ }
+
+ [Fact]
+ public void OwnMaterialCrossingRejects()
+ {
+ var clean = ExplicitContourTests.Square(false);
+ var prepared = PreparedContours.Capture(clean, ExplicitContourTests.Parameters());
+ var choice = prepared.Entry(0, 0, new Vector(0, 5));
+ // Own snapshot is where the sheet actually is; the placement drifts 0.4 east, so the
+ // emitted external lead (approach x=-0.21..0 relative, entry at x=0.4 absolute)
+ // crosses into the sheet's own material.
+ var own = LeadMaterialSnapshot.Capture(clean, Vector.Zero);
+ var feasibility = new ContourEntryFeasibility(prepared, new Vector(0.4, 0), own, []);
+
+ var verdict = feasibility.Check(choice);
+
+ Assert.Equal(ContourFeasibilityStatus.Blocked, verdict.Status);
+ Assert.False(verdict.IsClear);
+ }
+
+ [Fact]
+ public void IncompleteMaterialNeverReadsAsClear()
+ {
+ var (prepared, choice, _) = PreparedSquare();
+ // Two touching rings: the capture itself refuses, producing an incomplete snapshot.
+ var joined = ExplicitContourTests.Square(false);
+ var touching = new Program();
+ touching.MoveTo(0, 10);
+ touching.LineTo(10, 10); touching.LineTo(10, 20); touching.LineTo(0, 20); touching.LineTo(0, 10);
+ joined.Codes.AddRange(touching.Codes);
+ var incomplete = LeadMaterialSnapshot.Capture(joined, Vector.Zero);
+ Assert.False(incomplete.IsComplete);
+ Assert.NotNull(incomplete.Reason);
+
+ var ownVerdict = new ContourEntryFeasibility(prepared, At, incomplete, []).Check(choice);
+ Assert.Equal(ContourFeasibilityStatus.Incomplete, ownVerdict.Status);
+ var otherVerdict = new ContourEntryFeasibility(prepared, At,
+ LeadMaterialSnapshot.Capture(ExplicitContourTests.Square(false), At), [incomplete]).Check(choice);
+ Assert.Equal(ContourFeasibilityStatus.Incomplete, otherVerdict.Status);
+ }
+
+ [Fact]
+ public void NoLeadInClearVerdictIsNotAPlanApproval()
+ {
+ var parameters = ExplicitContourTests.Parameters();
+ parameters.ExternalLeadIn = new NoLeadIn();
+ parameters.InternalLeadIn = new NoLeadIn();
+ parameters.ArcCircleLeadIn = new NoLeadIn();
+ var clean = ExplicitContourTests.Square(false);
+ var prepared = PreparedContours.Capture(clean, parameters);
+ var choice = prepared.Entry(0, 0, new Vector(0, 5));
+ var feasibility = new ContourEntryFeasibility(prepared, At, LeadMaterialSnapshot.Capture(clean, At), []);
+
+ var verdict = feasibility.Check(choice);
+
+ // Nothing to certify means the validator passes vacuously — the emitted program
+ // carries no lead motion at all, so this verdict certifies no lead and the
+ // complete-plan missing-lead check still has to run later. The adapter must not
+ // silently drop the distinction: consumers can see it is vacuous by counting leads.
+ Assert.True(verdict.IsClear);
+ var motions = ExecutionMotionReader.Read(prepared.Emit(new[] { choice }), At, null, default).Motions;
+ Assert.Empty(motions.Where(m => !m.Rapid
+ && (m.Layer == LayerType.Leadin || m.Layer == LayerType.Leadout)));
+ }
+
+ [Fact]
+ public void MalformedEmissionRefusesWithReasonInsteadOfCrashing()
+ {
+ var (prepared, choice, own) = PreparedSquare();
+ var (other, _, _) = PreparedSquare();
+ var forgedContour = choice with { Owner = other, ContourOrdinal = 99 };
+ var forgedEntity = choice with { Owner = prepared, EntityOrdinal = 99 };
+ var feasibility = new ContourEntryFeasibility(prepared, At, own, []);
+
+ var verdict = feasibility.Check(forgedContour);
+ Assert.Equal(ContourFeasibilityStatus.Incomplete, verdict.Status);
+ Assert.False(string.IsNullOrEmpty(verdict.Reason));
+ Assert.Equal(ContourFeasibilityStatus.Incomplete, feasibility.Check(forgedEntity).Status);
+ }
+
+ [Fact]
+ public void CancellationPropagatesAndPoisonsNothing()
+ {
+ var (prepared, choice, own) = PreparedSquare();
+ var feasibility = new ContourEntryFeasibility(prepared, At, own, []);
+ using var cancelled = new CancellationTokenSource();
+ cancelled.Cancel();
+
+ Assert.ThrowsAny(() => feasibility.Check(choice, token: cancelled.Token));
+ Assert.Equal(0, feasibility.EvaluationCount);
+ Assert.True(feasibility.Check(choice).IsClear);
+ }
+
+ [Fact]
+ public void SameChoiceAndNodeContextIsCachedWithinTheAttemptOnly()
+ {
+ var (prepared, choice, own) = PreparedSquare();
+ var feasibility = new ContourEntryFeasibility(prepared, At, own, []);
+
+ Assert.True(feasibility.Check(choice, "A").IsClear);
+ Assert.True(feasibility.Check(choice, "A").IsClear);
+ Assert.Equal(1, feasibility.EvaluationCount);
+
+ // A different node context is a different key (the cache is deliberately conservative).
+ Assert.True(feasibility.Check(choice, "B").IsClear);
+ Assert.Equal(2, feasibility.EvaluationCount);
+
+ // Nothing survives into a fresh planning attempt: no static or cross-instance cache.
+ var fresh = new ContourEntryFeasibility(prepared, At, own, []);
+ Assert.True(fresh.Check(choice, "A").IsClear);
+ Assert.Equal(1, fresh.EvaluationCount);
+ }
+
+ [Fact]
+ public void EvaluationIsLazyPerCandidate()
+ {
+ var (prepared, choice, own) = PreparedSquare();
+ var feasibility = new ContourEntryFeasibility(prepared, At, own, []);
+ var other = prepared.Entry(0, 1, new Vector(5, 10));
+
+ // One Check evaluates exactly that candidate — never every point of every contour.
+ Assert.True(feasibility.Check(other).IsClear);
+ Assert.Equal(1, feasibility.EvaluationCount);
+ Assert.NotSame(choice, other);
+ }
+
+ [Fact]
+ public void VerdictsAreTheValidatorsVerdictsExactly()
+ {
+ // Adapter verdict == LeadPathValidator outcome for the same emitted program.
+ var (prepared, choice, own) = PreparedSquare();
+ var materials = new[] { own };
+ var execution = ExecutionMotionReader.Read(
+ prepared.EmitCandidateForValidation(choice), At, null, default);
+ var direct = LeadPathValidator.Check(execution, own, materials, default);
+
+ var verdict = new ContourEntryFeasibility(prepared, At, own, materials).Check(choice);
+
+ Assert.Equal(direct.IsClear, verdict.IsClear);
+ Assert.Equal(direct.Reason, verdict.Reason);
+ }
+}