Files
ParkingRobot/docs/superpowers/plans/2026-07-31-local-g2-soft-anchor-candidate-recovery.md
T

39 KiB
Raw Blame History

Local G2 Soft-Anchor Candidate Recovery Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Prove and implement a bounded soft-anchor Local G2 candidate family that makes the real SingleTurn route pass the unchanged safety/quality evaluator, then finish the dedicated Task 8 service pipeline.

Architecture: First preserve the already-proven evaluator seam correction, then make window generation cover representative total lengths before asymmetric variants consume the budget. A disposable-copy feasibility gate evaluates the exact soft-anchor family before the shared production builder changes; only a successful non-zero-offset candidate permits the builder TDD and Task 8 service integration to continue.

Tech Stack: C# 10, .NET Standard 2.0, PowerShell reflection verification, existing PathGeometryAnalyzer, SmoothedPathValidator, full-body collision checks, Git.

Global Constraints

  • MinimumWindowLengthMeters = 0.20, PreferredWindowLengthMeters = 0.50, and MaximumWindowLengthMeters = 0.80 are total left-plus-right lengths.
  • Path start, path end, gear switches, and outer window endpoints remain hard position anchors.
  • Only an internal primitive-boundary position may move; its vehicle heading and distance-weighted shared curvature remain hard boundary values.
  • softOffset = min(0.05 m, 0.5 × MaximumDeviationMeters); if it is at most 1e-10 m, emit no non-zero profile.
  • Per selected representative window, profile order is exact anchor, +softOffset, -softOffset, all with derivative multiplier 1.00.
  • The configured candidate limit remains authoritative and is capped by the existing hard maximum of 12.
  • Do not modify vehicle curvature limits, the validator 1e-6 tolerance, raw curvature-range tolerance, collision/clearance gates, 0.10 m default maximum deviation, 20 percent peak-gradient improvement, or 2 percent variation-cost tolerance.
  • Do not change Hybrid A*, SQP, legacy smoothing algorithms, or PathSmoothingComparisonRequest.DefaultMethods.
  • Detector report order remains immutable ascending order; processing uses LocalG2RegionWorkOrder.
  • Use TDD. Do not change LocalG2CandidateBuilder in the shared tree until the disposable feasibility gate finds an accepted non-zero-offset candidate under the unchanged evaluator.
  • Preserve unrelated dirty worktree files. Stage only the exact files listed by each task.
  • The untracked ClumsyPilot/ParkrobTrajplanner/auto_avoidance tree currently requires unavailable external assemblies. Never edit or delete it; when it blocks the normal build, verify this work from a disposable copy that excludes only that directory.

File Structure

  • ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs
    • Preserve the analyzer-compatible 1e-10 m boundary-point de-duplication already proven by RED/GREEN evidence.
  • ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs
    • Expose preferred/minimum/maximum balanced targets before asymmetric variants and keep deterministic target coverage.
  • ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs
    • Select representative windows, apply the bounded normal soft-anchor profiles, and emit at most 12 deterministic geometries.
  • ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs
    • Consume work order, evaluate candidates, roll back failed global combinations, and publish raw baseline when no accepted replacement survives.
  • ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs
    • Dispatch only LocalG2Quintic to the dedicated pipeline while preserving legacy routes.
  • ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1
    • Prove target coverage, total-window semantics, deterministic order, and configured caps.
  • ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1
    • Prove the evaluator seam, feasibility tuple, soft-anchor geometry, candidate budget, G2, direction, and determinism.
  • ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1
    • Prove public service statuses, safe complete-path publication, two-region order, rollback, and cancellation.
  • ClumsyPilot/tests/verify_path_smoothing_service.ps1
    • Prove dedicated Local G2 dispatch without disturbing legacy method registration.

Task 1: Preserve the evaluator window-boundary correction

Files:

  • Modify: ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs
  • Modify: ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1
  • Evidence: .superpowers/sdd/local-g2-task-8-integration-report.md

Interfaces:

  • Consumes: interpolated window start/end points and source points from one PreparedDirectionSegment.

  • Produces: TryExtractWindow(...) output with exact interpolated endpoints and no consecutive positions within 1e-10 m.

  • Step 1: Separate the completed evaluator regression from the still-RED candidate-family assertion

Keep the real SingleTurn seam and this assertion:

Assert-Equal 0 $duplicateFailures.Count `
    'SingleTurn builder candidates must not fail evaluator window analysis due to duplicate or degenerate points.'

Remove only the current $acceptedCandidates collection and the assertion requiring an accepted candidate. Task 3 reintroduces the accepted non-zero-offset requirement after the feasibility gate.

  • Step 2: Confirm the recorded RED/GREEN evidence is complete

Read .superpowers/sdd/local-g2-task-8-integration-report.md and require both entries:

RED: Expected=0 Actual=6 duplicate/degenerate evaluator failures
GREEN: Path smoothing Local G2 candidate checks passed.

The production correction must remain exactly:

private const double WindowPointToleranceMeters = 1e-10d;

private static bool SamePosition(SmoothingPoint2D left, SmoothingPoint2D right)
{
    if (left == null || right == null) return false;
    double x = right.X - left.X;
    double y = right.Y - left.Y;
    return x * x + y * y <=
        WindowPointToleranceMeters * WindowPointToleranceMeters;
}

and TryExtractWindow must preserve the exact interpolated end:

if (point.ArcLength > startArcLength &&
    point.ArcLength < endArcLength &&
    !SamePosition(points[points.Count - 1], point))
{
    points.Add(point);
}

if (SamePosition(points[points.Count - 1], end))
    points[points.Count - 1] = end;
else
    points.Add(end);
  • Step 3: Build and run the focused GREEN check

Run the normal commands first:

dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1

If the build fails only because of untracked auto_avoidance dependencies, create a disposable verification copy:

$verificationRoot = Join-Path $env:TEMP ('parkingrobot-local-g2-' + [Guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $verificationRoot | Out-Null
robocopy 'ClumsyPilot' (Join-Path $verificationRoot 'ClumsyPilot') /E /XD 'ClumsyPilot\ParkrobTrajplanner\auto_avoidance'
if ($LASTEXITCODE -gt 7) { throw "robocopy failed with $LASTEXITCODE" }
dotnet build (Join-Path $verificationRoot 'ClumsyPilot\ClumsyPilot.csproj') --no-restore
powershell -ExecutionPolicy Bypass -File (Join-Path $verificationRoot 'ClumsyPilot\tests\verify_path_smoothing_local_g2_candidates.ps1')

Expected: build has zero errors; candidate checks pass without an accepted-candidate requirement.

  • Step 4: Commit only the evaluator seam
git add -- `
  ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs `
  ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1
git diff --cached --check
git diff --cached --name-only
git commit -m "fix: deduplicate Local G2 evaluator windows"

Expected staged names: exactly the two files above.


Task 2: Guarantee representative window-target coverage

Files:

  • Modify: ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs
  • Modify: ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1

Interfaces:

  • Consumes: LocalG2OptionsSnapshot total-window bounds and MaximumCandidatesPerRegion.

  • Produces: ordered WindowVariants whose balanced target pass precedes 40/60 and 60/40 passes.

  • Step 1: Add the failing coverage scenario

Add this case to LocalG2WindowPlanner.TestHooks.Execute:

case "InteriorCoverage":
    transitions = new[] { Transition(1d, 0) };
    segmentLength = 2d;
    break;

Extend WindowPlanningTestSnapshot with:

public int FirstRegionVariantCount { get; }
public bool RepresentativeTargetsFirst { get; }
public bool HasAsymmetricVariant { get; }

Append the three values to its internal constructor and assign them exactly:

internal WindowPlanningTestSnapshot(
    int regionCount,
    string transitionCounts,
    double maximumWindowLength,
    bool exactEnvelope,
    double firstLeftLength,
    double firstRightLength,
    string signature,
    int firstRegionVariantCount,
    bool representativeTargetsFirst,
    bool hasAsymmetricVariant)
{
    RegionCount = regionCount;
    TransitionCounts = transitionCounts;
    MaximumWindowLength = maximumWindowLength;
    ExactEnvelope = exactEnvelope;
    FirstLeftLength = firstLeftLength;
    FirstRightLength = firstRightLength;
    Signature = signature;
    FirstRegionVariantCount = firstRegionVariantCount;
    RepresentativeTargetsFirst = representativeTargetsFirst;
    HasAsymmetricVariant = hasAsymmetricVariant;
}

Compute the values before returning the snapshot:

IReadOnlyList<LocalG2WindowVariant> firstVariants = regions[0].WindowVariants;
bool representativeTargetsFirst =
    firstVariants.Count >= 3 &&
    Math.Abs(WindowLength(firstVariants[0]) - 0.50d) <= 1e-9d &&
    Math.Abs(WindowLength(firstVariants[1]) - 0.20d) <= 1e-9d &&
    Math.Abs(WindowLength(firstVariants[2]) - 0.80d) <= 1e-9d;
bool hasAsymmetricVariant = false;
for (int index = 0; index < firstVariants.Count; index++)
{
    if (Math.Abs(
            firstVariants[index].LeftWindowLengthMeters -
            firstVariants[index].RightWindowLengthMeters) > 1e-9d)
    {
        hasAsymmetricVariant = true;
        break;
    }
}

Add the helper:

private static double WindowLength(LocalG2WindowVariant variant) =>
    variant.EndArcLengthMeters - variant.StartArcLengthMeters;

Pass the three values through the snapshot constructor. In the PowerShell verifier add:

$coverage = $executeMethod.Invoke($null, @('InteriorCoverage'))
Assert-True $coverage.RepresentativeTargetsFirst `
    'Preferred, minimum, and maximum balanced targets must precede asymmetric variants.'
Assert-True $coverage.HasAsymmetricVariant `
    'Default window planning must retain a legal asymmetric variant after balanced coverage.'
Assert-True ($coverage.FirstRegionVariantCount -le 12) `
    'Window planning must obey the configured default cap.'
  • Step 2: Run the detection verifier to prove RED

Run:

dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1

Use the disposable-copy command from Task 1 only if the normal build is blocked by auto_avoidance.

Expected RED: RepresentativeTargetsFirst is false because the current order begins 0.50, 0.50, 0.50 for balanced/asymmetric splits of one target.

  • Step 3: Implement coverage-pass enumeration

Replace BuildVariants with:

private static IReadOnlyList<LocalG2WindowVariant> BuildVariants(
    IReadOnlyList<CurvatureTransition> transitions,
    double segmentLength,
    LocalG2OptionsSnapshot options)
{
    var variants = new List<LocalG2WindowVariant>();
    double firstEvent = transitions[0].LocalArcLengthMeters;
    double lastEvent = transitions[transitions.Count - 1].LocalArcLengthMeters;
    double anchor = (firstEvent + lastEvent) / 2d;
    IReadOnlyList<double> targets = BuildTargets(options, segmentLength);
    double[] ratios = { 0.5d, 0.4d, 0.6d };
    for (int ratioIndex = 0; ratioIndex < ratios.Length; ratioIndex++)
    {
        for (int targetIndex = 0; targetIndex < targets.Count; targetIndex++)
        {
            if (variants.Count >= options.MaximumCandidatesPerRegion)
                return new ReadOnlyCollection<LocalG2WindowVariant>(variants);
            AddIfLegal(
                variants,
                targets[targetIndex],
                ratios[ratioIndex],
                anchor,
                firstEvent,
                lastEvent,
                segmentLength,
                ratioIndex == 0,
                options);
        }
    }
    return new ReadOnlyCollection<LocalG2WindowVariant>(variants);
}

Replace the requested array in BuildTargets with:

double[] requested =
{
    options.PreferredWindowLengthMeters,
    options.MinimumWindowLengthMeters,
    options.MaximumWindowLengthMeters,
    0.75d * options.PreferredWindowLengthMeters,
    1.25d * options.PreferredWindowLengthMeters,
};

Do not alter AddIfLegal, total-length validation, grouping, or region-envelope calculation.

  • Step 4: Verify GREEN and determinism

Run twice:

powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1

Expected: all pass; both detection runs publish identical signatures.

  • Step 5: Commit the planner coverage change
git add -- `
  ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2WindowPlanner.cs `
  ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1
git diff --cached --check
git diff --cached --name-only
git commit -m "fix: cover Local G2 window targets before splits"

Task 3: Prove and implement the soft-anchor candidate family

Files:

  • Modify after feasibility GREEN: ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs
  • Modify: ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1
  • Evidence: .superpowers/sdd/local-g2-soft-anchor-feasibility-report.md

Interfaces:

  • Consumes: stabilized WindowVariants, internal CurvatureTransition anchors, travel direction, and MaximumDeviationMeters.

  • Produces: profile-major deterministic candidates with exact outer states and bounded normal movement at internal anchors.

  • Step 1: Reintroduce the permanent RED acceptance seam

Before the real-candidate loop initialize:

$acceptedSoftCandidates = @()
$candidateSignatures = @()

For each real candidate, find its point at the first transition arc within 1e-9 m, then compute:

$transition = (Get-InternalProperty $region 'Transitions')[0]
$travelHeading = [double](Get-InternalProperty $transition 'VehicleHeadingRadians')
$normalX = -[Math]::Sin($travelHeading)
$normalY = [Math]::Cos($travelHeading)
$softOffset = 0.0
foreach ($point in (Get-InternalProperty $realCandidate 'RegionPoints')) {
    if ([Math]::Abs($point.ArcLength - (Get-InternalProperty $transition 'LocalArcLengthMeters')) -le 1e-9) {
        $softOffset = ($point.X - (Get-InternalProperty $transition 'X')) * $normalX +
            ($point.Y - (Get-InternalProperty $transition 'Y')) * $normalY
        break
    }
}
if ((Get-InternalProperty $evaluation 'Accepted') -and [Math]::Abs($softOffset) -gt 1e-10) {
    $acceptedSoftCandidates += $realCandidate
}

Add:

Assert-True ($acceptedSoftCandidates.Count -gt 0) `
    'SingleTurn must produce an accepted non-zero soft-anchor candidate under unchanged gates.'

Run the candidate verifier against the current builder. Expected RED: no non-zero soft-anchor candidate exists.

  • Step 2: Run the disposable feasibility gate before shared production edits

Create a new disposable verification copy using Task 1's command. In that copy only, apply the complete builder changes from Steps 3 and 4 below with apply_patch. Add temporary output inside the candidate evaluation loop:

$candidateIndex = Get-InternalProperty $realCandidate 'CandidateIndex'
$startArc = Get-InternalProperty $realCandidate 'StartArcLengthMeters'
$endArc = Get-InternalProperty $realCandidate 'EndArcLengthMeters'
$accepted = Get-InternalProperty $evaluation 'Accepted'
$failure = Get-InternalProperty $evaluation 'FailureReason'
$rawPeak = Get-InternalProperty $evaluation 'RawPeakCurvatureDerivativePerSquareMeter'
$resultPeak = Get-InternalProperty $evaluation 'ResultPeakCurvatureDerivativePerSquareMeter'
$maximumCurvature = Get-InternalProperty $evaluation 'MaximumAbsoluteVehicleCurvaturePerMeter'
$maximumDeviation = Get-InternalProperty $evaluation 'MaximumDeviationMeters'
$minimumClearance = Get-InternalProperty $evaluation 'MinimumBodyClearanceMeters'
Write-Output ("soft-probe candidate=$candidateIndex window=$startArc..$endArc offset=$softOffset accepted=$accepted failure=$failure rawPeak=$rawPeak resultPeak=$resultPeak maxCurvature=$maximumCurvature deviation=$maximumDeviation clearance=$minimumClearance")

In the disposable verifier, also calculate the raw and candidate curvature ranges with the evaluator's unchanged private analysis path:

function Get-AnalysisCurvatureRange($Analysis) {
    $minimum = [double]::PositiveInfinity
    $maximum = [double]::NegativeInfinity
    foreach ($point in (Get-InternalProperty $Analysis 'Path')) {
        $minimum = [Math]::Min($minimum, [double]$point.VehicleCurvature)
        $maximum = [Math]::Max($maximum, [double]$point.VehicleCurvature)
    }
    return [PSCustomObject]@{ Minimum = $minimum; Maximum = $maximum }
}

$extractMethod = Get-InternalMethod $evaluatorType 'TryExtractWindow'
$analyzeMethod = Get-InternalMethod $evaluatorType 'TryAnalyzeWindow'
$extractArgs = [object[]]@($preparedSegment, [double]$startArc, [double]$endArc, $null, $null)
Assert-True ($extractMethod.Invoke($realEvaluator, $extractArgs)) `
    'The feasibility probe must extract the unchanged raw window.'
$rawWindow = $extractArgs[3]
$rawAnalyzeArgs = [object[]]@(
    $preparedSegment, $rawWindow, [double]$configuration.OutputSpacingMeters, $null, $null)
Assert-True ($analyzeMethod.Invoke($realEvaluator, $rawAnalyzeArgs)) `
    'The feasibility probe must analyze the unchanged raw window.'
$candidateAnalyzeArgs = [object[]]@(
    $preparedSegment,
    (Get-InternalProperty $realCandidate 'RegionPoints'),
    [double]$configuration.OutputSpacingMeters,
    $null,
    $null)
Assert-True ($analyzeMethod.Invoke($realEvaluator, $candidateAnalyzeArgs)) `
    'The feasibility probe must analyze the soft-anchor candidate window.'
$rawRange = Get-AnalysisCurvatureRange $rawAnalyzeArgs[3]
$candidateRange = Get-AnalysisCurvatureRange $candidateAnalyzeArgs[3]
Write-Output ("soft-probe ranges candidate=$candidateIndex raw=$($rawRange.Minimum)..$($rawRange.Maximum) candidate=$($candidateRange.Minimum)..$($candidateRange.Maximum)")

Run:

dotnet build (Join-Path $verificationRoot 'ClumsyPilot\ClumsyPilot.csproj') --no-restore
powershell -ExecutionPolicy Bypass -File (Join-Path $verificationRoot 'ClumsyPilot\tests\verify_path_smoothing_local_g2_candidates.ps1')

Write the exact command, every tuple line, and the final accepted tuple to .superpowers/sdd/local-g2-soft-anchor-feasibility-report.md using apply_patch.

Gate result:

  • GREEN: at least one line has accepted=True, abs(offset) > 1e-10, result peak at most 80 percent of raw peak, and all unchanged evaluator gates pass. Continue.

  • RED: no such line exists. Stop this plan, leave the shared builder unchanged, and report the design as blocked. Do not change expected fixture statuses or any threshold.

  • Step 3: Implement representative-window selection after feasibility GREEN

Replace the current window-first/scale-second loop in Build with:

IReadOnlyList<LocalG2WindowVariant> windows =
    SelectRepresentativeWindows(region.WindowVariants);
double softOffset = Math.Min(0.05d, 0.5d * options.MaximumDeviationMeters);
double[] offsets = softOffset <= 1e-10d
    ? new[] { 0d }
    : new[] { 0d, softOffset, -softOffset };
var candidates = new List<LocalG2CandidateGeometry>();
int limit = Math.Min(options.MaximumCandidatesPerRegion, 12);
for (int profileIndex = 0; profileIndex < offsets.Length; profileIndex++)
{
    for (int windowIndex = 0; windowIndex < windows.Count; windowIndex++)
    {
        cancellationToken.ThrowIfCancellationRequested();
        if (candidates.Count >= limit) return ReadOnly(candidates);
        if (TryBuildCandidate(
                candidates.Count,
                originalSegment,
                region,
                windows[windowIndex],
                1d,
                offsets[profileIndex],
                outputSpacingMeters,
                cancellationToken,
                out LocalG2CandidateGeometry candidate))
        {
            candidates.Add(candidate);
        }
    }
}
return ReadOnly(candidates);

Add these helpers next to SameArc:

private static IReadOnlyList<LocalG2WindowVariant> SelectRepresentativeWindows(
    IReadOnlyList<LocalG2WindowVariant> variants)
{
    var selected = new List<LocalG2WindowVariant>(4);
    if (variants == null || variants.Count == 0)
        return new ReadOnlyCollection<LocalG2WindowVariant>(selected);

    AddDistinctWindow(selected, variants[0]);
    LocalG2WindowVariant minimum = variants[0];
    LocalG2WindowVariant maximum = variants[0];
    LocalG2WindowVariant asymmetric = variants[0];
    for (int index = 1; index < variants.Count; index++)
    {
        LocalG2WindowVariant candidate = variants[index];
        if (IsShorter(candidate, minimum)) minimum = candidate;
        if (IsLonger(candidate, maximum)) maximum = candidate;
        if (IsMoreAsymmetric(candidate, asymmetric)) asymmetric = candidate;
    }
    AddDistinctWindow(selected, minimum);
    AddDistinctWindow(selected, maximum);
    AddDistinctWindow(selected, asymmetric);
    return new ReadOnlyCollection<LocalG2WindowVariant>(selected);
}

private static bool IsShorter(
    LocalG2WindowVariant candidate,
    LocalG2WindowVariant current)
{
    double candidateLength =
        candidate.EndArcLengthMeters - candidate.StartArcLengthMeters;
    double currentLength =
        current.EndArcLengthMeters - current.StartArcLengthMeters;
    return candidateLength < currentLength - 1e-10d ||
        (Math.Abs(candidateLength - currentLength) <= 1e-10d &&
         candidate.CandidateIndex < current.CandidateIndex);
}

private static bool IsLonger(
    LocalG2WindowVariant candidate,
    LocalG2WindowVariant current)
{
    double candidateLength =
        candidate.EndArcLengthMeters - candidate.StartArcLengthMeters;
    double currentLength =
        current.EndArcLengthMeters - current.StartArcLengthMeters;
    return candidateLength > currentLength + 1e-10d ||
        (Math.Abs(candidateLength - currentLength) <= 1e-10d &&
         candidate.CandidateIndex < current.CandidateIndex);
}

private static bool IsMoreAsymmetric(
    LocalG2WindowVariant candidate,
    LocalG2WindowVariant current)
{
    double candidateValue = Math.Abs(
        candidate.LeftWindowLengthMeters - candidate.RightWindowLengthMeters);
    double currentValue = Math.Abs(
        current.LeftWindowLengthMeters - current.RightWindowLengthMeters);
    return candidateValue > currentValue + 1e-10d ||
        (Math.Abs(candidateValue - currentValue) <= 1e-10d &&
         candidate.CandidateIndex < current.CandidateIndex);
}

private static void AddDistinctWindow(
    List<LocalG2WindowVariant> selected,
    LocalG2WindowVariant candidate)
{
    for (int index = 0; index < selected.Count; index++)
    {
        if (SameArc(selected[index].StartArcLengthMeters, candidate.StartArcLengthMeters) &&
            SameArc(selected[index].EndArcLengthMeters, candidate.EndArcLengthMeters))
        {
            return;
        }
    }
    selected.Add(candidate);
}
  • Step 4: Implement bounded internal-anchor movement

Add double normalOffsetMeters to TryBuildCandidate immediately after double multiplier.

Replace internal node construction with:

double travelHeading = segment.Direction == TravelDirection.Forward
    ? transition.VehicleHeadingRadians
    : transition.VehicleHeadingRadians - Math.PI;
double normalX = -Math.Sin(travelHeading);
double normalY = Math.Cos(travelHeading);
double nodeX = transition.X + normalOffsetMeters * normalX;
double nodeY = transition.Y + normalOffsetMeters * normalY;
if (!NumericGuard.IsFinite(nodeX) || !NumericGuard.IsFinite(nodeY))
    return false;
nodes.Add(new BoundaryNode(
    transition.LocalArcLengthMeters,
    nodeX,
    nodeY,
    transition.VehicleHeadingRadians,
    sharedCurvature));

Declare this immediately before the transition loop:

int internalNodeCount = 0;

Increment it immediately after adding each moved internal node. Before TryAssignDerivativeScales add:

if (internalNodeCount == 0 && Math.Abs(normalOffsetMeters) > 1e-10d)
    return false;

Keep the original interpolated outer nodes, distance-weighted sharedCurvature, derivative-scale calculation, derivative certification, sampling, direction sign, and candidate endpoint fields unchanged. Delete the now-unused DerivativeScaleMultipliers field.

  • Step 5: Complete permanent soft-anchor and budget assertions

Add assertions that:

Assert-True ($acceptedSoftCandidates.Count -gt 0) `
    'SingleTurn must publish an accepted non-zero soft-anchor candidate.'
Assert-True ($realCandidates.Count -le $configuration.LocalG2Quintic.MaximumCandidatesPerRegion) `
    'Builder output must obey the configured candidate cap.'

Run the real builder twice and compare, for every candidate, candidate index, start/end arc, point count, every X/Y/Heading/Source value, and the inferred signed internal offset. Loop candidate limits from 1 through 12; rebuild options and assert output count never exceeds the configured limit. Set MaximumDeviationMeters = 0.02 in one request and assert every inferred anchor offset is at most 0.010000001 m.

Extend the existing cluster and reverse TestHook cases so that one non-zero profile proves:

outer endpoint position error <= 1e-9 m
outer endpoint heading error <= 1e-9 rad
outer endpoint curvature error <= 1e-8 1/m
internal left/right tangent error <= 1e-8
internal left/right curvature error <= 1e-8 1/m
  • Step 6: Verify candidate GREEN and integration progress

Run:

dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1

Use the disposable copy when the normal build is blocked only by auto_avoidance.

Expected: detection and candidate scripts pass; service integration advances past single-turn must publish its required Local G2 status. Any later Task 8 failure is handled in Task 4 without modifying candidate gates.

  • Step 7: Commit the soft-anchor candidate family
git add -- `
  ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs `
  ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1
git diff --cached --check
git diff --cached --name-only
git commit -m "fix: generate bounded Local G2 soft anchors"

Task 4: Finish the dedicated Task 8 pipeline and service publication

Files:

  • Create from the existing untracked prototype: ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs
  • Modify: ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs
  • Create from the existing untracked prototype: ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1
  • Modify: ClumsyPilot/tests/verify_path_smoothing_service.ps1

Interfaces:

  • Consumes: verified RawPathBaseline, immutable prepared path, detector reportOrder, LocalG2RegionWorkOrder, candidate builder/evaluator.
  • Produces:
internal PathSmoothingResult Smooth(
    PathSmoothingRequest request,
    PreparedPath preparedPath,
    RawPathBaseline rawBaseline,
    CancellationToken cancellationToken);
  • Guarantees: every valid non-cancelled request publishes a complete verified path; invalid raw input and cancellation publish no partial path.

  • Step 1: Run the public integration and service scripts as RED

Run:

dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_service.ps1

Expected before the final pipeline correction: at least one mandatory fixture/status, raw-baseline publication, rollback, or report assertion fails. Record the first expected failure in .superpowers/sdd/local-g2-task-8-integration-report.md.

  • Step 2: Preserve immutable report order and work-order processing

Keep this exact flow in Smooth:

IReadOnlyList<LocalG2SmoothingRegion> reportOrder =
    new ReadOnlyCollection<LocalG2SmoothingRegion>(
        new List<LocalG2SmoothingRegion>(regions));
if (!_workOrder.TryCreate(
        reportOrder,
        out IReadOnlyList<LocalG2SmoothingRegion> workRegions,
        out string orderReason))
{
    return Failure(PathSmoothingStatus.Failed, stopwatch, orderReason);
}

foreach (LocalG2SmoothingRegion region in workRegions)
{
    cancellationToken.ThrowIfCancellationRequested();
    // Build from preparedPath; evaluate against current.
}

var reports = new List<PathSmoothingRegionReport>(reportOrder.Count);
for (int reportIndex = 0; reportIndex < reportOrder.Count; reportIndex++)
    reports.Add(reportsByRegion[reportOrder[reportIndex]]);

Candidate construction must use preparedPath.Segments[region.SegmentIndex]; evaluation must receive both preparedPath and the evolving current path.

  • Step 3: Publish the verified raw baseline without reconstructing it

Before regional processing, if transitions.Count == 0, publish:

return Publish(
    PathSmoothingStatus.NotNeeded,
    rawBaseline,
    stopwatch,
    new List<PathSmoothingRegionReport>());

Add:

private static PathSmoothingResult Publish(
    PathSmoothingStatus status,
    RawPathBaseline baseline,
    Stopwatch stopwatch,
    IReadOnlyList<PathSmoothingRegionReport> reports) =>
    PathSmoothingResult.PublishLocalG2(
        status,
        baseline.Path,
        baseline.Segments,
        new PathSmoothingDiagnostics(
            baseline.Metrics,
            stopwatch.Elapsed,
            0,
            0d,
            string.Empty),
        reports);

After regional evaluation and global rollback, if improvedCount == 0, publish the same rawBaseline as Unchanged with detector-order reports. Do not call PathGeometryAnalyzer again for this raw fallback; the pre-Task-8 trusted-curvature gate already produced and fully validated it.

  • Step 4: Make accepted and retained reports describe the actual candidate

For every region retain:

IReadOnlyList<LocalG2CandidateGeometry> candidates
LocalG2CandidateEvaluation best
LocalG2CandidateGeometry selectedCandidate

Find the selected geometry deterministically:

LocalG2CandidateGeometry selectedCandidate = null;
for (int index = 0; index < candidates.Count; index++)
{
    if (candidates[index].CandidateIndex == best.CandidateIndex)
    {
        selectedCandidate = candidates[index];
        break;
    }
}

When best.Accepted, require selectedCandidate != null, store it in AcceptedRegion, and report its start/end/left/right lengths. When no candidate is accepted, use candidate index -1, the first window only as planned-window diagnostics, and the stable failure reason from best.

Extend AcceptedRegion with:

internal int CandidateCount { get; }
internal LocalG2CandidateGeometry Candidate { get; }

Rollback reports use the stored candidate count and selected candidate rather than region.WindowVariants.Count.

  • Step 5: Preserve global rollback and exact status derivation

After candidate processing, validate current. If it fails, roll accepted regions back in reverse acceptance order. After each rollback, re-run full validation. Mark every removed region GlobalValidationRollback.

Derive status only after rollback:

if (improvedCount == 0)
    status = PathSmoothingStatus.Unchanged;
else if (improvedCount == regions.Count)
    status = PathSmoothingStatus.Complete;
else
    status = PathSmoothingStatus.PartialImprovement;

Complete and PartialImprovement must have at least one Improved report. If every accepted region is rolled back, publish the verified rawBaseline as Unchanged.

  • Step 6: Keep service dispatch isolated

In PathSmoothingService retain the dedicated branch after common validation, preparation, and raw-baseline creation:

if (configuration.Method == SmoothingMethod.LocalG2Quintic)
    return _localG2Pipeline.Smooth(
        request,
        preparedPath,
        rawBaseline,
        cancellationToken);

Validate LocalG2QuinticOptions only for that method; legacy methods continue through Resolve(...) and _runner. Do not add LocalG2Quintic to comparison defaults.

  • Step 7: Complete integration assertions

The integration script must run every fixture twice and require:

Straight -> NotNeeded
SingleTurn -> Complete
LargeHeadingChange -> Complete
SBend -> Complete or PartialImprovement
RectangleDetour -> Complete or PartialImprovement
MultiObstacleDetour -> Complete or PartialImprovement
ReverseGearSwitch -> Complete, PartialImprovement, or NotNeeded

For every published path require complete segment coverage, feasible diagnostics, configured clearance, deterministic status/coordinates/reports, and at least one Improved report for Complete/PartialImprovement. Keep the strict 0.99 improvement request as Unchanged, cancellation as empty-path Cancelled, and the same-direction two-region case as back-to-front processing with detector-order reports.

  • Step 8: Run focused and legacy GREEN checks

Run:

dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_service.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_runner.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_integration.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_detection.ps1

Expected: all pass. Use the disposable copy only for the unrelated auto_avoidance build blocker.

  • Step 9: Commit only Task 8 publication files
git add -- `
  ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs `
  ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs `
  ClumsyPilot/tests/verify_path_smoothing_local_g2_integration.ps1 `
  ClumsyPilot/tests/verify_path_smoothing_service.ps1
git diff --cached --check
git diff --cached --name-only
git commit -m "feat: publish Local G2 presmoothing results"

Expected staged names: exactly the four files above. If the service file contains unrelated user hunks, stage the Task 8 patch into the index without staging those hunks and verify with git diff --cached before committing.


Task 5: Run the complete recovery and Task 8 verification gate

Files:

  • Verify: all files committed by Tasks 14
  • Verify: all ClumsyPilot/tests/verify_path_smoothing_*.ps1
  • Record: .superpowers/sdd/local-g2-soft-anchor-task-8-final-report.md

Interfaces:

  • Consumes: the evaluator seam, window scheduling, proven soft-anchor family, and dedicated pipeline.

  • Produces: evidence that Task 8 is complete without modifying Task 9 documentation.

  • Step 1: Build the final source

dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore

Expected: zero errors. The two pre-existing obsolete API warnings may remain. If unrelated auto_avoidance dependencies still block the shared build, also record that exact failure and run the same build in the disposable copy.

  • Step 2: Run every PathSmoothing verification script
$tests = @(
  'verify_path_smoothing_algorithm_input.ps1',
  'verify_path_smoothing_bezier.ps1',
  'verify_path_smoothing_bspline.ps1',
  'verify_path_smoothing_comparison.ps1',
  'verify_path_smoothing_contracts.ps1',
  'verify_path_smoothing_documentation.ps1',
  'verify_path_smoothing_fixtures.ps1',
  'verify_path_smoothing_geometry.ps1',
  'verify_path_smoothing_integration.ps1',
  'verify_path_smoothing_local_g2_candidates.ps1',
  'verify_path_smoothing_local_g2_curve.ps1',
  'verify_path_smoothing_local_g2_detection.ps1',
  'verify_path_smoothing_local_g2_integration.ps1',
  'verify_path_smoothing_png.ps1',
  'verify_path_smoothing_quintic.ps1',
  'verify_path_smoothing_runner.ps1',
  'verify_path_smoothing_service.ps1',
  'verify_path_smoothing_svg_csv.ps1',
  'verify_path_smoothing_validation.ps1'
)
foreach ($test in $tests) {
    & powershell -ExecutionPolicy Bypass -File (Join-Path 'ClumsyPilot/tests' $test)
    if ($LASTEXITCODE -ne 0) { throw "$test failed with exit code $LASTEXITCODE" }
}

Expected: all 19 scripts pass.

  • Step 3: Audit scope, safety thresholds, and Task 9 boundary
git diff --check
git diff --cached --check
git status --short
git log -6 --oneline
rg -n "MaximumCurvaturePerMeter|CurvatureRangeTolerance|MinimumClearanceReserveMeters|MinimumPeakGradientImprovementRatio|MaximumVariationCostRegressionRatio" `
  ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2 `
  ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs

Confirm no safety value changed, no staged files remain, unrelated dirty files are untouched, comparison defaults exclude Local G2, and Task 9 README/documentation work has not started.

  • Step 4: Record final evidence without an empty commit

Write .superpowers/sdd/local-g2-soft-anchor-task-8-final-report.md with:

Soft-anchor feasibility gate: PASS with exact accepted tuple and unchanged evaluator metrics.
Window target coverage: PASS for preferred/minimum/maximum before asymmetric variants.
SingleTurn: Complete with at least one accepted non-zero soft anchor.
Task 8 service publication: PASS including raw fallback, rollback, work/report order, and cancellation.
Full PathSmoothing verification: 19/19 scripts PASS (or shared-build limitation plus isolated-copy evidence explicitly recorded).
Task 9 documentation remains pending.

Do not create a verification-only commit.