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

972 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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:
```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<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:
```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<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`:
```csharp
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:
```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<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:
```csharp
return Publish(
PathSmoothingStatus.NotNeeded,
rawBaseline,
stopwatch,
new List<PathSmoothingRegionReport>());
```
Add:
```csharp
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:
```csharp
IReadOnlyList<LocalG2CandidateGeometry> 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 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**
```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.