diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/IPathSmoother.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/IPathSmoother.cs
new file mode 100644
index 0000000..1a9c251
--- /dev/null
+++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/IPathSmoother.cs
@@ -0,0 +1,14 @@
+using System.Threading;
+
+namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Algorithms;
+
+/// 单一平滑方法生成原始几何候选的内部契约。
+internal interface IPathSmoother
+{
+ SmoothingMethod Method { get; }
+
+ SmoothingCandidate Smooth(
+ SmoothingAlgorithmInput input,
+ double effectiveStrength,
+ CancellationToken cancellationToken);
+}
diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmInput.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmInput.cs
new file mode 100644
index 0000000..87c52cf
--- /dev/null
+++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmInput.cs
@@ -0,0 +1,34 @@
+using System;
+using MultiWheelC.TrajectoryPlanning.CoarsePath;
+using MultiWheelC.TrajectoryPlanning.Mapping;
+using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing;
+
+namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Algorithms;
+
+/// 单次算法运行共享的已预处理路径和独立复核上下文。
+internal sealed class SmoothingAlgorithmInput
+{
+ internal SmoothingAlgorithmInput(
+ PreparedPath originalPath,
+ PlanningGridMap map,
+ VehicleParameters vehicle,
+ double maximumCollisionCheckStepMeters)
+ {
+ OriginalPath = originalPath ?? throw new ArgumentNullException(nameof(originalPath));
+ Map = map ?? throw new ArgumentNullException(nameof(map));
+ Vehicle = vehicle ?? throw new ArgumentNullException(nameof(vehicle));
+ MaximumCollisionCheckStepMeters = maximumCollisionCheckStepMeters;
+ }
+
+ /// 已校验并按方向分段的原始路径。
+ internal PreparedPath OriginalPath { get; }
+
+ /// 用于完整车体复核的不可变规划地图。
+ internal PlanningGridMap Map { get; }
+
+ /// 用于曲率和足迹复核的车辆参数快照。
+ internal VehicleParameters Vehicle { get; }
+
+ /// 连续车体碰撞检查的最大步长,单位 m。
+ internal double MaximumCollisionCheckStepMeters { get; }
+}
diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmRunner.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmRunner.cs
new file mode 100644
index 0000000..9f24798
--- /dev/null
+++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingAlgorithmRunner.cs
@@ -0,0 +1,446 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Threading;
+using MultiWheelC.TrajectoryPlanning.CoarsePath;
+using MultiWheelC.TrajectoryPlanning.Mapping;
+using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing;
+using MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation;
+
+namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Algorithms;
+
+/// 在有限强度计划内运行单一算法,并以共享分析和安全复核决定是否接受候选。
+internal sealed class SmoothingAlgorithmRunner
+{
+ private readonly PathGeometryAnalyzer _analyzer;
+ private readonly SmoothedPathValidator _validator;
+
+ internal SmoothingAlgorithmRunner()
+ : this(new PathGeometryAnalyzer(), new SmoothedPathValidator())
+ {
+ }
+
+ internal SmoothingAlgorithmRunner(PathGeometryAnalyzer analyzer, SmoothedPathValidator validator)
+ {
+ _analyzer = analyzer ?? throw new ArgumentNullException(nameof(analyzer));
+ _validator = validator ?? throw new ArgumentNullException(nameof(validator));
+ }
+
+ ///
+ /// 依次尝试配置的有限强度比例。取消直接向上传播,由门面转换为最终状态;
+ /// 数值失败和几何分析失败均不重试,以避免以较低强度掩盖算法退化。
+ ///
+ internal AlgorithmRunResult Run(
+ IPathSmoother smoother,
+ SmoothingAlgorithmInput input,
+ PathSmoothingConfiguration configuration,
+ CancellationToken cancellationToken)
+ {
+ var attemptedStrengths = new List();
+ var failureReasons = new List();
+ if (smoother == null || input == null || configuration == null)
+ return AlgorithmRunResult.Failed("平滑算法、输入或配置无效。", attemptedStrengths, failureReasons);
+ if (configuration.RetryStrengthScales == null || configuration.RetryStrengthScales.Count == 0)
+ return AlgorithmRunResult.Failed("平滑强度重试计划为空。", attemptedStrengths, failureReasons);
+
+ SmoothingCandidate rejectedCandidate = null;
+ foreach (double scale in configuration.RetryStrengthScales)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ double effectiveStrength = configuration.SmoothingStrength * scale;
+ if (!IsPositiveFinite(effectiveStrength))
+ return AlgorithmRunResult.Failed("平滑强度或重试比例无效。", attemptedStrengths, failureReasons);
+
+ attemptedStrengths.Add(effectiveStrength);
+ SmoothingCandidate candidate = smoother.Smooth(input, effectiveStrength, cancellationToken);
+ if (candidate == null)
+ return AlgorithmRunResult.Failed("平滑算法未返回候选。", attemptedStrengths, failureReasons);
+ if (!candidate.Succeeded)
+ {
+ failureReasons.Add(candidate.Reason);
+ return AlgorithmRunResult.Failed(candidate.Reason, attemptedStrengths, failureReasons);
+ }
+
+ if (!_analyzer.TryAnalyze(candidate.Segments, configuration.OutputSpacingMeters,
+ out PathGeometryAnalysis analysis, out string reason))
+ {
+ failureReasons.Add(reason);
+ return AlgorithmRunResult.Failed(reason, attemptedStrengths, failureReasons);
+ }
+
+ if (_validator.TryValidate(analysis.Path, analysis.Segments, input.OriginalPath, input.Map, input.Vehicle,
+ input.MaximumCollisionCheckStepMeters, out IReadOnlyList safePath,
+ out double minimumClearanceMeters, out reason))
+ {
+ return AlgorithmRunResult.Success(safePath, analysis.Segments,
+ CreateMetrics(analysis, minimumClearanceMeters), effectiveStrength,
+ attemptedStrengths, failureReasons);
+ }
+
+ rejectedCandidate = candidate;
+ failureReasons.Add(reason);
+ }
+
+ return AlgorithmRunResult.Infeasible(rejectedCandidate, attemptedStrengths, failureReasons);
+ }
+
+ private static PathQualityMetrics CreateMetrics(PathGeometryAnalysis analysis, double minimumClearanceMeters)
+ {
+ return new PathQualityMetrics(
+ true,
+ analysis.PathLengthMeters,
+ analysis.MaximumAbsoluteVehicleCurvaturePerMeter,
+ analysis.RootMeanSquareVehicleCurvaturePerMeter,
+ analysis.TotalAbsoluteCurvatureVariationPerMeter,
+ analysis.CurvatureVariationEnergy,
+ minimumClearanceMeters,
+ 0d,
+ 0d,
+ 0d,
+ 0d);
+ }
+
+ private static bool IsPositiveFinite(double value)
+ {
+ return !double.IsNaN(value) && !double.IsInfinity(value) && value > 0d;
+ }
+
+ ///
+ /// Reflection-only deterministic coverage seam. It is nested in an internal runner and intentionally
+ /// does not construct or register a production smoothing method.
+ ///
+ public static class TestHooks
+ {
+ /// 执行一个固定的内部假平滑器场景并返回可反射读取的快照。
+ public static RunnerTestSnapshot Execute(string scenario)
+ {
+ if (string.IsNullOrWhiteSpace(scenario)) throw new ArgumentException("A scenario is required.", nameof(scenario));
+
+ var cancellationSource = new CancellationTokenSource();
+ var smoother = new DeterministicTestSmoother(ParseScenario(scenario), cancellationSource);
+ var runner = new SmoothingAlgorithmRunner();
+ SmoothingAlgorithmInput input = CreateTestInput();
+ var configuration = new PathSmoothingConfiguration();
+ try
+ {
+ AlgorithmRunResult result = runner.Run(smoother, input, configuration, cancellationSource.Token);
+ return RunnerTestSnapshot.FromResult(result, false);
+ }
+ catch (OperationCanceledException)
+ {
+ return new RunnerTestSnapshot(
+ "OperationCanceledException",
+ smoother.AttemptedStrengths,
+ 0,
+ 0,
+ 0,
+ true);
+ }
+ finally
+ {
+ cancellationSource.Dispose();
+ }
+ }
+
+ /// 供 PowerShell 断言使用的不可变执行摘要。
+ public sealed class RunnerTestSnapshot
+ {
+ internal RunnerTestSnapshot(
+ string status,
+ IReadOnlyList attemptedStrengths,
+ int acceptedPathPointCount,
+ int rejectedComparisonCandidatePointCount,
+ int failureCount,
+ bool cancellationPropagated)
+ {
+ Status = status ?? string.Empty;
+ AttemptedStrengths = CopyReadOnly(attemptedStrengths);
+ AcceptedPathPointCount = acceptedPathPointCount;
+ RejectedComparisonCandidatePointCount = rejectedComparisonCandidatePointCount;
+ FailureCount = failureCount;
+ CancellationPropagated = cancellationPropagated;
+ }
+
+ public string Status { get; }
+ public IReadOnlyList AttemptedStrengths { get; }
+ public int AcceptedPathPointCount { get; }
+ public int RejectedComparisonCandidatePointCount { get; }
+ public int FailureCount { get; }
+ public bool CancellationPropagated { get; }
+
+ internal static RunnerTestSnapshot FromResult(AlgorithmRunResult result, bool cancellationPropagated)
+ {
+ int rejectedPointCount = result.RejectedComparisonCandidate == null
+ ? 0
+ : CountPoints(result.RejectedComparisonCandidate.Segments);
+ return new RunnerTestSnapshot(
+ result.Status.ToString(),
+ result.AttemptedStrengths,
+ result.Path.Count,
+ rejectedPointCount,
+ result.FailureReasons.Count,
+ cancellationPropagated);
+ }
+
+ private static IReadOnlyList CopyReadOnly(IReadOnlyList source)
+ {
+ var copy = new List(source == null ? 0 : source.Count);
+ if (source != null)
+ {
+ for (int index = 0; index < source.Count; index++) copy.Add(source[index]);
+ }
+ return new ReadOnlyCollection(copy);
+ }
+ }
+
+ private enum TestScenario
+ {
+ RejectAll,
+ AcceptFirst,
+ NumericalFailure,
+ CancelBeforeNextAttempt,
+ }
+
+ private sealed class DeterministicTestSmoother : IPathSmoother
+ {
+ private readonly TestScenario _scenario;
+ private readonly CancellationTokenSource _cancellationSource;
+
+ internal DeterministicTestSmoother(TestScenario scenario, CancellationTokenSource cancellationSource)
+ {
+ _scenario = scenario;
+ _cancellationSource = cancellationSource;
+ AttemptedStrengths = new List();
+ }
+
+ public SmoothingMethod Method => SmoothingMethod.CubicBSpline;
+
+ internal List AttemptedStrengths { get; }
+
+ public SmoothingCandidate Smooth(
+ SmoothingAlgorithmInput input,
+ double effectiveStrength,
+ CancellationToken cancellationToken)
+ {
+ AttemptedStrengths.Add(effectiveStrength);
+ if (_scenario == TestScenario.NumericalFailure)
+ return SmoothingCandidate.Failed("确定性数值退化。");
+ if (_scenario == TestScenario.AcceptFirst)
+ return CreateAcceptedCandidate();
+
+ SmoothingCandidate rejected = CreateRejectedCandidate();
+ if (_scenario == TestScenario.CancelBeforeNextAttempt)
+ _cancellationSource.Cancel();
+ return rejected;
+ }
+ }
+
+ private static TestScenario ParseScenario(string scenario)
+ {
+ if (string.Equals(scenario, nameof(TestScenario.RejectAll), StringComparison.Ordinal)) return TestScenario.RejectAll;
+ if (string.Equals(scenario, nameof(TestScenario.AcceptFirst), StringComparison.Ordinal)) return TestScenario.AcceptFirst;
+ if (string.Equals(scenario, nameof(TestScenario.NumericalFailure), StringComparison.Ordinal)) return TestScenario.NumericalFailure;
+ if (string.Equals(scenario, nameof(TestScenario.CancelBeforeNextAttempt), StringComparison.Ordinal)) return TestScenario.CancelBeforeNextAttempt;
+ throw new ArgumentOutOfRangeException(nameof(scenario));
+ }
+
+ private static SmoothingAlgorithmInput CreateTestInput()
+ {
+ var mapRequest = new PlanningMapRequest
+ {
+ Bounds = new MapBoundsMm(0f, 5000f, 0f, 5000f),
+ ResolutionMm = 50f,
+ AllowExplicitEmptyMap = true,
+ };
+ PlanningMapBuildResult mapResult = new PlanningMapFactory().Create(mapRequest);
+ if (!mapResult.Succeeded || mapResult.Map == null)
+ throw new InvalidOperationException("The runner test hook could not create its empty map.");
+
+ var originalSegments = new List
+ {
+ new PreparedDirectionSegment(
+ 0,
+ TravelDirection.Forward,
+ new List
+ {
+ CreatePoint(0.5d, 0.5d, 0d),
+ CreatePoint(1.5d, 0.5d, 1d),
+ },
+ false,
+ false),
+ };
+ var vehicle = new VehicleParameters
+ {
+ LengthMeters = 0.20d,
+ WidthMeters = 0.20d,
+ SafetyMarginMeters = 0d,
+ MaximumCurvaturePerMeter = 100d,
+ MinimumTurningRadiusMeters = 0.01d,
+ };
+ return new SmoothingAlgorithmInput(new PreparedPath(originalSegments), mapResult.Map, vehicle, 0.05d);
+ }
+
+ private static SmoothingCandidate CreateAcceptedCandidate()
+ {
+ return SmoothingCandidate.Success(new List
+ {
+ new PreparedDirectionSegment(
+ 0,
+ TravelDirection.Forward,
+ new List
+ {
+ CreatePoint(0.5d, 0.5d, 0d),
+ CreatePoint(1.5d, 0.5d, 1d),
+ },
+ false,
+ false),
+ });
+ }
+
+ private static SmoothingCandidate CreateRejectedCandidate()
+ {
+ return SmoothingCandidate.Success(new List
+ {
+ new PreparedDirectionSegment(
+ 0,
+ TravelDirection.Forward,
+ new List
+ {
+ CreatePoint(0.5d, 0.5d, 0d),
+ CreatePoint(0.05d, 0.5d, 0.45d),
+ CreatePoint(1.5d, 0.5d, 1.90d),
+ },
+ false,
+ false),
+ });
+ }
+
+ private static SmoothingPoint2D CreatePoint(double x, double y, double arcLength)
+ {
+ return new SmoothingPoint2D(
+ x,
+ y,
+ arcLength,
+ 0d,
+ 0d,
+ 1d,
+ false,
+ SmoothedPathPointSource.Anchor);
+ }
+
+ private static int CountPoints(IReadOnlyList segments)
+ {
+ int count = 0;
+ for (int index = 0; index < segments.Count; index++) count += segments[index].Points.Count;
+ return count;
+ }
+ }
+
+ /// 内部运行结果;只有成功路径可被正式门面发布,拒绝候选仅供比较诊断读取。
+ internal sealed class AlgorithmRunResult
+ {
+ private static readonly IReadOnlyList EmptyPath =
+ new ReadOnlyCollection(new List());
+ private static readonly IReadOnlyList EmptySegments =
+ new ReadOnlyCollection(new List());
+
+ private AlgorithmRunResult(
+ PathSmoothingStatus status,
+ IReadOnlyList path,
+ IReadOnlyList segments,
+ PathQualityMetrics metrics,
+ double acceptedStrength,
+ SmoothingCandidate rejectedComparisonCandidate,
+ IReadOnlyList attemptedStrengths,
+ IReadOnlyList failureReasons,
+ string reason)
+ {
+ Status = status;
+ Path = path ?? EmptyPath;
+ Segments = segments ?? EmptySegments;
+ Metrics = metrics ?? new PathQualityMetrics();
+ AcceptedStrength = acceptedStrength;
+ RejectedComparisonCandidate = rejectedComparisonCandidate;
+ AttemptedStrengths = CopyReadOnly(attemptedStrengths);
+ FailureReasons = CopyReadOnly(failureReasons);
+ Reason = reason ?? string.Empty;
+ }
+
+ internal PathSmoothingStatus Status { get; }
+ internal IReadOnlyList Path { get; }
+ internal IReadOnlyList Segments { get; }
+ internal PathQualityMetrics Metrics { get; }
+ internal double AcceptedStrength { get; }
+ internal SmoothingCandidate RejectedComparisonCandidate { get; }
+ internal IReadOnlyList AttemptedStrengths { get; }
+ internal IReadOnlyList FailureReasons { get; }
+ internal string Reason { get; }
+
+ internal static AlgorithmRunResult Success(
+ IReadOnlyList path,
+ IReadOnlyList segments,
+ PathQualityMetrics metrics,
+ double acceptedStrength,
+ IReadOnlyList attemptedStrengths,
+ IReadOnlyList failureReasons)
+ {
+ return new AlgorithmRunResult(
+ PathSmoothingStatus.Success,
+ CopyReadOnly(path),
+ CopyReadOnly(segments),
+ metrics,
+ acceptedStrength,
+ null,
+ attemptedStrengths,
+ failureReasons,
+ string.Empty);
+ }
+
+ internal static AlgorithmRunResult Infeasible(
+ SmoothingCandidate rejectedComparisonCandidate,
+ IReadOnlyList attemptedStrengths,
+ IReadOnlyList failureReasons)
+ {
+ string reason = failureReasons == null || failureReasons.Count == 0
+ ? "所有有限平滑尝试均未通过复核。"
+ : failureReasons[failureReasons.Count - 1];
+ return new AlgorithmRunResult(
+ PathSmoothingStatus.Infeasible,
+ null,
+ null,
+ null,
+ 0d,
+ rejectedComparisonCandidate,
+ attemptedStrengths,
+ failureReasons,
+ reason);
+ }
+
+ internal static AlgorithmRunResult Failed(
+ string reason,
+ IReadOnlyList attemptedStrengths,
+ IReadOnlyList failureReasons)
+ {
+ return new AlgorithmRunResult(
+ PathSmoothingStatus.Failed,
+ null,
+ null,
+ null,
+ 0d,
+ null,
+ attemptedStrengths,
+ failureReasons,
+ reason);
+ }
+
+ private static IReadOnlyList CopyReadOnly(IReadOnlyList source)
+ {
+ var copy = new List(source == null ? 0 : source.Count);
+ if (source != null)
+ {
+ for (int index = 0; index < source.Count; index++) copy.Add(source[index]);
+ }
+ return new ReadOnlyCollection(copy);
+ }
+ }
+}
diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingCandidate.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingCandidate.cs
new file mode 100644
index 0000000..1016501
--- /dev/null
+++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/SmoothingCandidate.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing;
+
+namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Algorithms;
+
+/// 平滑方法产生的原始方向段候选,尚未经过统一几何或安全复核。
+internal sealed class SmoothingCandidate
+{
+ private SmoothingCandidate(bool succeeded, IReadOnlyList segments, string reason)
+ {
+ Succeeded = succeeded;
+ Segments = CopyReadOnly(segments);
+ Reason = reason ?? string.Empty;
+ }
+
+ /// 候选是否成功产生有限的原始几何。
+ internal bool Succeeded { get; }
+
+ /// 候选方向段;失败候选始终为空。
+ internal IReadOnlyList Segments { get; }
+
+ /// 失败或退化时的稳定说明;成功时为空。
+ internal string Reason { get; }
+
+ /// 创建待统一分析和验证的成功候选。
+ internal static SmoothingCandidate Success(IReadOnlyList segments)
+ {
+ if (segments == null || segments.Count == 0)
+ throw new ArgumentException("A successful smoothing candidate requires direction segments.", nameof(segments));
+ return new SmoothingCandidate(true, segments, string.Empty);
+ }
+
+ /// 创建不应重试的数值或构造失败候选。
+ internal static SmoothingCandidate Failed(string reason)
+ {
+ if (string.IsNullOrWhiteSpace(reason))
+ throw new ArgumentException("A failed smoothing candidate requires a reason.", nameof(reason));
+ return new SmoothingCandidate(false, null, reason);
+ }
+
+ private static IReadOnlyList CopyReadOnly(IReadOnlyList source)
+ {
+ var copy = new List(source == null ? 0 : source.Count);
+ if (source != null)
+ {
+ for (int index = 0; index < source.Count; index++) copy.Add(source[index]);
+ }
+ return new ReadOnlyCollection(copy);
+ }
+}
diff --git a/ClumsyPilot/tests/verify_path_smoothing_runner.ps1 b/ClumsyPilot/tests/verify_path_smoothing_runner.ps1
new file mode 100644
index 0000000..cad1830
--- /dev/null
+++ b/ClumsyPilot/tests/verify_path_smoothing_runner.ps1
@@ -0,0 +1,73 @@
+param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll'))
+
+$ErrorActionPreference = 'Stop'
+$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath))
+
+function Assert-True($Actual, [string]$Message) {
+ if (-not $Actual) { throw $Message }
+}
+
+function Assert-False($Actual, [string]$Message) {
+ if ($Actual) { throw $Message }
+}
+
+function Assert-Equal($Expected, $Actual, [string]$Message) {
+ if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" }
+}
+
+function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) {
+ if ([Math]::Abs($Expected - $Actual) -gt 0.000000001) {
+ throw "$Message Expected=$Expected Actual=$Actual"
+ }
+}
+
+function Get-RequiredType([string]$Name) {
+ return $assembly.GetType($Name, $true)
+}
+
+function Assert-AttemptedStrengths($Snapshot, [double[]]$Expected, [string]$Message) {
+ Assert-Equal $Expected.Length $Snapshot.AttemptedStrengths.Count ($Message + ' count')
+ for ($index = 0; $index -lt $Expected.Length; $index++) {
+ Assert-Near $Expected[$index] $Snapshot.AttemptedStrengths[$index] ($Message + " index=$index")
+ }
+}
+
+$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.'
+$algorithms = $root + 'Algorithms.'
+$runnerType = Get-RequiredType ($algorithms + 'SmoothingAlgorithmRunner')
+$smootherType = Get-RequiredType ($algorithms + 'IPathSmoother')
+Assert-False $smootherType.IsPublic 'IPathSmoother must remain internal to the algorithm assembly.'
+
+$hooksType = $runnerType.GetNestedType('TestHooks', [Reflection.BindingFlags]'Public,NonPublic')
+Assert-True ($null -ne $hooksType) 'SmoothingAlgorithmRunner must expose its narrowly scoped nested TestHooks helper.'
+$executeMethod = $hooksType.GetMethod('Execute', [Reflection.BindingFlags]'Public,Static')
+Assert-True ($null -ne $executeMethod) 'TestHooks must expose deterministic scenario execution for reflection tests.'
+
+function Invoke-Scenario([string]$Scenario) {
+ return $executeMethod.Invoke($null, @($Scenario))
+}
+
+$allRejected = Invoke-Scenario 'RejectAll'
+Assert-Equal 'Infeasible' $allRejected.Status 'All rejected finite candidates must produce an Infeasible runner result.'
+Assert-AttemptedStrengths $allRejected @(1.00, 0.75, 0.50, 0.25) 'Rejected candidates must use the finite retry schedule exactly.'
+Assert-Equal 0 $allRejected.AcceptedPathPointCount 'An infeasible runner result must not retain a rejected candidate as an accepted path.'
+Assert-True ($allRejected.RejectedComparisonCandidatePointCount -gt 0) 'Only comparison diagnostics may retain the last rejected candidate geometry.'
+Assert-Equal 4 $allRejected.FailureCount 'Every rejected validation attempt must retain its failure reason.'
+
+$accepted = Invoke-Scenario 'AcceptFirst'
+Assert-Equal 'Success' $accepted.Status 'The first safe candidate must be accepted.'
+Assert-AttemptedStrengths $accepted @(1.00) 'The runner must stop immediately after the first accepted candidate.'
+Assert-True ($accepted.AcceptedPathPointCount -gt 0) 'A successful runner result must publish the validated path internally.'
+Assert-Equal 0 $accepted.RejectedComparisonCandidatePointCount 'An accepted candidate must not create rejected comparison geometry.'
+
+$numericalFailure = Invoke-Scenario 'NumericalFailure'
+Assert-Equal 'Failed' $numericalFailure.Status 'A numerical candidate failure must stop the runner as Failed.'
+Assert-AttemptedStrengths $numericalFailure @(1.00) 'Numerical candidate failure must not retry at lower strength.'
+Assert-Equal 0 $numericalFailure.RejectedComparisonCandidatePointCount 'A non-candidate numerical failure must not retain comparison geometry.'
+
+$cancelled = Invoke-Scenario 'CancelBeforeNextAttempt'
+Assert-True $cancelled.CancellationPropagated 'Cancellation between attempts must propagate out of the runner.'
+Assert-AttemptedStrengths $cancelled @(1.00) 'Cancellation before the next attempt must prevent another smoother call.'
+Assert-Equal 0 $cancelled.AcceptedPathPointCount 'A cancelled run must not publish a partial path.'
+
+Write-Output 'Path smoothing retry runner checks passed.'