# 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: ```powershell 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: ```text RED: Expected=0 Actual=6 duplicate/degenerate evaluator failures GREEN: Path smoothing Local G2 candidate checks passed. ``` The production correction must remain exactly: ```csharp 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: ```csharp 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: ```powershell 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: ```powershell $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** ```powershell 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`: ```csharp case "InteriorCoverage": transitions = new[] { Transition(1d, 0) }; segmentLength = 2d; break; ``` Extend `WindowPlanningTestSnapshot` with: ```csharp public int FirstRegionVariantCount { get; } public bool RepresentativeTargetsFirst { get; } public bool HasAsymmetricVariant { get; } ``` Append the three values to its internal constructor and assign them exactly: ```csharp 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: ```csharp IReadOnlyList 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: ```csharp private static double WindowLength(LocalG2WindowVariant variant) => variant.EndArcLengthMeters - variant.StartArcLengthMeters; ``` Pass the three values through the snapshot constructor. In the PowerShell verifier add: ```powershell $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: ```powershell 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: ```csharp private static IReadOnlyList BuildVariants( IReadOnlyList transitions, double segmentLength, LocalG2OptionsSnapshot options) { var variants = new List(); double firstEvent = transitions[0].LocalArcLengthMeters; double lastEvent = transitions[transitions.Count - 1].LocalArcLengthMeters; double anchor = (firstEvent + lastEvent) / 2d; IReadOnlyList 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(variants); AddIfLegal( variants, targets[targetIndex], ratios[ratioIndex], anchor, firstEvent, lastEvent, segmentLength, ratioIndex == 0, options); } } return new ReadOnlyCollection(variants); } ``` Replace the `requested` array in `BuildTargets` with: ```csharp 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 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** ```powershell 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: ```powershell $acceptedSoftCandidates = @() $candidateSignatures = @() ``` For each real candidate, find its point at the first transition arc within `1e-9 m`, then compute: ```powershell $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: ```powershell 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: ```powershell $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: ```powershell 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: ```powershell 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: ```csharp IReadOnlyList 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(); 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`: ```csharp private static IReadOnlyList SelectRepresentativeWindows( IReadOnlyList variants) { var selected = new List(4); if (variants == null || variants.Count == 0) return new ReadOnlyCollection(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(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 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: ```csharp 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: ```csharp int internalNodeCount = 0; ``` Increment it immediately after adding each moved internal node. Before `TryAssignDerivativeScales` add: ```csharp 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: ```powershell 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: ```text 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: ```powershell 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** ```powershell 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: ```csharp 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: ```powershell 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`: ```csharp IReadOnlyList reportOrder = new ReadOnlyCollection( new List(regions)); if (!_workOrder.TryCreate( reportOrder, out IReadOnlyList 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(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: ```csharp return Publish( PathSmoothingStatus.NotNeeded, rawBaseline, stopwatch, new List()); ``` Add: ```csharp private static PathSmoothingResult Publish( PathSmoothingStatus status, RawPathBaseline baseline, Stopwatch stopwatch, IReadOnlyList 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: ```csharp IReadOnlyList candidates LocalG2CandidateEvaluation best LocalG2CandidateGeometry selectedCandidate ``` Find the selected geometry deterministically: ```csharp 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: ```csharp 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: ```csharp 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: ```csharp 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: ```text 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: ```powershell 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** ```powershell 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 1–4 - 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** ```powershell 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** ```powershell $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** ```powershell 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: ```text 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.