104 KiB
Local G2 Minimum Curvature Excursion Feasibility 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: In two disposable roots, determine whether a hard-anchor, arc-length curvature-Bézier Local G2 candidate can pass every existing SingleTurn safety and quality gate except the strict raw-curvature-range early return, and report the smallest excursion found within the fixed search budget.
Architecture: A disposable probe-core Git repository owns the analytic curvature curve, the bounded two-variable pose-closure solver, the temporary candidate builder, deterministic diagnostics, and the shared probe script. Task 4 clones the same frozen core commit into a strict root whose evaluator is byte-identical to the shared baseline and a measurement root whose evaluator changes only the raw-range early return plus non-behavioral diagnostics; candidate SHA-256 records bind the two results atomically. No probe source is merged back: the shared branch receives only this plan and the final evidence report.
Tech Stack: C# 10, .NET Standard 2.0, PowerShell 5.1 reflection tests, System.Security.Cryptography.SHA256, existing Local G2 preprocessing/evaluation/validation components, local disposable Git repositories.
Global Constraints
- This plan implements only the isolated feasibility probe authorized by
docs/superpowers/specs/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-design.md; it does not authorize production candidate/evaluator/configuration changes. - Path start, path end, gear switches, outer window endpoints, and every internal primitive boundary remain hard position anchors.
- Internal primitive boundaries retain the original vehicle heading and the existing distance-weighted shared vehicle curvature.
- Work in travel geometry:
geometricCurvature = directionSign * vehicleCurvature, with forwarddirectionSign=+1and reversedirectionSign=-1. - Per interval,
kappa(u)is cubic Bézier;c2 = 4*deltaHeading/L - c0 - c1 - c3; the only solver variables are[L,c1]. - All
c0,c1,c2,c3must be finite and within[-Kvehicle,+Kvehicle]; analyzer-discrete vehicle curvature must still pass the unchanged vehicle maximum-curvature gate. - Before exact-anchor output overwrite, Euclidean position closure is
<=1e-9 m; analytic heading error is<=1e-8 rad; endpoint/seam curvature error is<=1e-8 1/m. - Candidate deviation remains
<=0.10 m; full-body collision, map boundary, clearance, 20 percent peak-gradient improvement, and 2 percent variation-cost tolerance remain unchanged. - The strict root evaluator is byte-identical to the recorded shared baseline. The measurement root may only record-and-continue at the raw-range early return and add diagnostics that do not reorder or change any other gate.
- Use the ordered states
PROBE_INVALID,BUDGET_RED,STRICT_GREEN,NO_PHYSICALLY_ADMISSIBLE_CLOSED_CANDIDATE_FOUND_WITHIN_BUDGET,QUALITY_RED, andFEASIBLE_WITH_EXCURSION; exactly one must result. - Search limits are 12 windows, 36 window/seed combinations, 8 intervals per combination, 96 total solver invocations including failures, 16 outer LM iterations, 4 damping trials per iteration, and integration depth 10.
- Performance protocol is 5 warm-ups plus 30 measured runs. Solver-phase median/worst must be
<=10/25 ms; measurement-root region median/worst must be<=25/50 ms. - Identical measured runs must have identical candidate keys, candidate SHA-256 values, evaluator results, state, and diagnostic selection.
- Use TDD in the disposable repository. Every implementation task commits there and receives an independent task review before the next task.
- Preserve the shared dirty worktree. On the shared branch, stage only this plan or the final report named in Task 5.
- Never read, modify, copy, delete, or commit
ClumsyPilot/ParkrobTrajplanner/auto_avoidance; verify it is absent from every disposable root before building. - A cancellation is
PROBE_INVALID, never a geometry RED. Do not use wall-clock time as a solver stop condition.
Execution workspace contract
Task 1 creates one disposable Git repository under the system temp directory and records its absolute path and baseline hashes in .superpowers/sdd/local-g2-excursion-task-1-report.md. The controller passes that exact path to Tasks 2 and 3. Task 4 clones the frozen Task 3 core commit twice and records strict/measurement paths in .superpowers/sdd/local-g2-excursion-task-4-report.md.
All Create and Modify paths in Tasks 1–4 are relative to the disposable root named in the task report, except .superpowers/sdd/*, which always resolves against the original shared repository. Those report files match the existing .superpowers/sdd/.gitignore, are never written inside strict/measurement clones, and must never be staged; the surrounding .superpowers/ directory may remain unrelated untracked user state.
File Structure
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/ArcLengthCurvatureBezierCurve2D.cs- Owns cubic curvature coefficients, analytic heading, adaptive 8/16 Gauss–Legendre coordinate integration, exact analytic curvature range/gradient diagnostics, and curve sampling.
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PoseClosureSolver.cs- Owns the joint physical-curvature domain, three seeds, finite differences, projected two-variable LM, fixed failure exits, and per-interval diagnostics.
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2ExcursionProbeDiagnostics.cs- Owns immutable interval/attempt/candidate diagnostics used only by the disposable probe.
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs- Temporarily attaches one
LocalG2ExcursionCandidateDiagnosticwithout changing existing geometry semantics.
- Temporarily attaches one
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs- Temporarily replaces the Hermite candidate family with bounded curvature-domain interval solving and enforces all attempt limits.
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs- Passes the already-computed vehicle maximum curvature into the temporary builder; no evaluator gate changes.
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs- Unchanged in core/strict roots. Measurement-root-only commit records the raw range and continues, while preserving every later gate.
ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_curve.ps1- Verifies curve math, integration, analytic extrema, finite inputs, and fixed integration boundaries.
ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_solver.ps1- Verifies analytic heading elimination, joint domain, exact LM constants/exits, cancellation, limits, and deterministic solutions.
ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1- Runs the real fixture, emits stable candidate keys/digests and gate/audit JSON, runs 5+30 timing, and applies the exhaustive state machine.
docs/superpowers/reports/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-report.md- The only probe output copied to and committed on the shared branch.
Task 1: Create the disposable core and implement the analytic curvature curve
Files:
- Create in disposable core:
.gitignore - Create in disposable core:
.gitattributes - Create in disposable core:
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/ArcLengthCurvatureBezierCurve2D.cs - Create in disposable core:
ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_curve.ps1 - Record without committing:
.superpowers/sdd/local-g2-excursion-task-1-report.md
Interfaces:
- Produces:
internal sealed class ArcLengthCurvatureBezierCurve2D
{
internal static bool TryCreate(
double startX, double startY, double startTravelHeadingRadians,
double lengthMeters, double c0, double c1, double c2, double c3,
out ArcLengthCurvatureBezierCurve2D curve, out string reason);
internal double LengthMeters { get; }
internal double StartCurvaturePerMeter { get; }
internal double EndCurvaturePerMeter { get; }
internal double EvaluateCurvature(double u);
internal double EvaluateCurvatureDerivativePerSquareMeter(double u);
internal double EvaluateTravelHeading(double u);
internal bool TryEvaluate(double u, CancellationToken cancellationToken,
out CurvatureCurveSample sample, out CurvatureIntegrationDiagnostic diagnostic,
out string reason);
internal bool TryGetAnalyticExtrema(out double minimumCurvature,
out double maximumCurvature, out double maximumAbsoluteGradient);
public sealed class CurvatureCurveTestSnapshot
{
public double EndX { get; }
public double EndY { get; }
public double EndHeading { get; }
public double MinimumCurvature { get; }
public double MaximumCurvature { get; }
public double MaximumEndpointCurvature { get; }
public bool AllValuesFinite { get; }
public bool AnalyticMatchesDenseReference { get; }
public bool GradientMatchesDenseReference { get; }
public bool EqualToleranceAccepted { get; }
public bool DepthTenFailureIsStable { get; }
public bool CancellationPropagates { get; }
public bool InvalidInputsRejected { get; }
}
public static class TestHooks
{
public static CurvatureCurveTestSnapshot Execute(string scenario);
}
}
internal readonly struct CurvatureCurveSample
{
internal CurvatureCurveSample(double parameter, double x, double y,
double travelHeadingRadians, double curvaturePerMeter);
internal double Parameter { get; }
internal double X { get; }
internal double Y { get; }
internal double TravelHeadingRadians { get; }
internal double CurvaturePerMeter { get; }
}
internal readonly struct CurvatureIntegrationDiagnostic
{
internal CurvatureIntegrationDiagnostic(int evaluationCount, int maximumDepth);
internal int EvaluationCount { get; }
internal int MaximumDepth { get; }
}
-
Consumers: Task 2 uses curve endpoint integration as the position residual; Task 3 uses
TryEvaluatefor candidate sampling. -
Step 1: Create and fingerprint the disposable core before changing code
Run from the shared repository root:
$sourceProject = (Resolve-Path 'ClumsyPilot').Path
$excludedAuto = [IO.Path]::GetFullPath((Join-Path $sourceProject 'ParkrobTrajplanner\auto_avoidance'))
$probeRoot = Join-Path ([IO.Path]::GetTempPath()) ('local-g2-excursion-' + [Guid]::NewGuid().ToString('N'))
$coreRoot = Join-Path $probeRoot 'probe-core'
$coreProject = Join-Path $coreRoot 'ClumsyPilot'
$probeEvidenceRoot = Join-Path $probeRoot 'evidence'
New-Item -ItemType Directory -Path $coreProject -Force | Out-Null
New-Item -ItemType Directory -Path $probeEvidenceRoot -Force | Out-Null
robocopy $sourceProject $coreProject /E /XD $excludedAuto bin obj .task8-sweep | Out-Null
if ($LASTEXITCODE -gt 7) { throw "robocopy failed with exit code $LASTEXITCODE" }
if (Test-Path (Join-Path $coreProject 'ParkrobTrajplanner\auto_avoidance')) {
throw 'auto_avoidance was copied into the disposable core.'
}
Use apply_patch in $coreRoot to create .gitignore with exactly:
**/bin/
**/obj/
Create .gitattributes with exactly:
* -text
This disposable-only policy preserves every copied file's raw bytes and prevents the machine-level core.autocrlf setting from changing LF/CRLF bytes between the core and later clones. It does not modify the shared repository's attributes.
Then initialize and commit the snapshot:
git -C $coreRoot init
git -C $coreRoot config core.autocrlf false
if ((git -C $coreRoot config --get core.autocrlf).Trim() -ne 'false') {
throw 'Disposable core must pin core.autocrlf=false.'
}
git -C $coreRoot add -- .gitattributes .gitignore ClumsyPilot
git -C $coreRoot commit -m 'chore: capture Local G2 excursion probe baseline'
Record in the task report:
shared HEAD
shared dirty-worktree status
absolute probeRoot/coreRoot
core baseline commit
SHA-256 of shared and core LocalG2CandidateEvaluator.cs
SHA-256 of path-smoothing-fixtures.json
sorted path + SHA-256 manifest for ClumsyPilot.csproj,
every .cs file below ParkrobTrajplanner/PathSmoothing,
and ParkrobTrajplanner/PathSmoothing/Test/Fixtures/path-smoothing-fixtures.json
confirmation that auto_avoidance is absent
Build the manifest by enumerating only those explicit paths; never enumerate ParkrobTrajplanner/auto_avoidance. Map each shared relative path to the copied core path and require identical file counts, paths, and hashes before continuing. The two evaluator hashes must match before continuing. Store the machine-readable manifest under $probeRoot/evidence/shared-baseline-manifest.json and its SHA-256 in the Task 1 scratch report.
- Step 2: Write the failing curve test
Create verify_path_smoothing_local_g2_curvature_probe_curve.ps1. It must load the disposable ClumsyPilot.dll, reflect ArcLengthCurvatureBezierCurve2D.TestHooks.Execute(string), and assert these exact scenarios:
$straight = Invoke-Scenario 'Straight'
Assert-Near 2.0 $straight.EndX 1e-11 'Straight X endpoint must integrate exactly.'
Assert-Near 0.0 $straight.EndY 1e-11 'Straight Y endpoint must remain zero.'
Assert-Near 0.0 $straight.EndHeading 1e-12 'Straight heading must remain zero.'
$circle = Invoke-Scenario 'ConstantCurvature'
Assert-Near ([Math]::Sin(0.4) / 0.4) $circle.EndX 1e-11 'Circle X must match.'
Assert-Near ((1.0 - [Math]::Cos(0.4)) / 0.4) $circle.EndY 1e-11 'Circle Y must match.'
Assert-Near 0.4 $circle.EndHeading 1e-12 'Circle heading integral must match.'
$counter = Invoke-Scenario 'ZeroHeadingCounterCurvature'
Assert-Near 0.0 $counter.EndHeading 1e-12 'Bezier heading elimination identity must hold.'
Assert-True ($counter.MinimumCurvature -lt 0.0) 'Counter-curvature scenario must go negative.'
Assert-Near 0.4 $counter.MaximumCurvature 1e-12 'Counter-curvature endpoint must remain 0.4 /m.'
$overshoot = Invoke-Scenario 'PositiveInteriorOvershoot'
Assert-True ($overshoot.MaximumCurvature -gt $overshoot.MaximumEndpointCurvature) `
'A non-monotone control polygon must expose positive interior overshoot.'
Assert-True $overshoot.AllValuesFinite 'Positive-overshoot diagnostics must remain finite.'
$extrema = Invoke-Scenario 'AnalyticExtrema'
Assert-True $extrema.AnalyticMatchesDenseReference 'Analytic extrema must match a dense reference.'
Assert-True $extrema.GradientMatchesDenseReference 'Analytic gradient peak must match a dense reference.'
$limits = Invoke-Scenario 'IntegrationLimits'
Assert-True $limits.EqualToleranceAccepted 'Error equal to 1e-12 m must be accepted.'
Assert-True $limits.DepthTenFailureIsStable 'Depth 10 above tolerance must fail closed.'
Assert-True $limits.CancellationPropagates 'Cancellation must propagate.'
Assert-True $limits.InvalidInputsRejected 'Non-finite/illegal inputs must be rejected.'
- Step 3: Run RED
dotnet build ClumsyPilot/ClumsyPilot.csproj --configuration Debug
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_curve.ps1
Expected: build or reflection FAIL because ArcLengthCurvatureBezierCurve2D does not exist. A pass means the seam is not exercising the new component.
- Step 4: Implement the cubic curvature and analytic heading
Use the polynomial coefficients exactly:
_a0 = c0;
_a1 = 3d * (c1 - c0);
_a2 = 3d * (c0 - 2d * c1 + c2);
_a3 = -c0 + 3d * c1 - 3d * c2 + c3;
internal double EvaluateCurvature(double u) =>
((_a3 * u + _a2) * u + _a1) * u + _a0;
internal double EvaluateTravelHeading(double u) => _startHeading + _length *
(_a0 * u + _a1 * u * u / 2d + _a2 * u * u * u / 3d +
_a3 * u * u * u * u / 4d);
internal double EvaluateCurvatureDerivativePerSquareMeter(double u) =>
(_a1 + 2d * _a2 * u + 3d * _a3 * u * u) / _length;
TryGetAnalyticExtrema evaluates curvature at u=0, u=1, and every finite derivative root in [0,1]. It evaluates absolute gradient at u=0, u=1, and the quadratic-gradient vertex u=-_a2/(3*_a3) when finite and in range.
- Step 5: Implement fixed adaptive 8/16 Gauss–Legendre integration
Use these positive nodes/weights and mirror them around zero:
private static readonly double[] Nodes8 =
{ 0.1834346424956498d, 0.5255324099163290d, 0.7966664774136267d, 0.9602898564975363d };
private static readonly double[] Weights8 =
{ 0.3626837833783620d, 0.3137066458778873d, 0.2223810344533745d, 0.1012285362903763d };
private static readonly double[] Nodes16 =
{ 0.09501250983763744d, 0.2816035507792589d, 0.4580167776572274d,
0.6178762444026438d, 0.7554044083550030d, 0.8656312023878318d,
0.9445750230732326d, 0.9894009349916499d };
private static readonly double[] Weights16 =
{ 0.1894506104550685d, 0.1826034150449236d, 0.1691565193950025d,
0.1495959888165767d, 0.1246289712555339d, 0.0951585116824928d,
0.06225352393864789d, 0.02715245941175409d };
For cos(theta(u)) and sin(theta(u)), compute the 8- and 16-point normalized vector integrals over the current u subinterval. Accept when L * EuclideanNorm(vector16-vector8) <= 1e-12 m; otherwise bisect while depth<10; at depth==10 return IntegrationDepthLimit. Multiply the accepted normalized integral by L exactly once when producing coordinates.
- Step 6: Run GREEN twice and commit in the disposable core
dotnet build ClumsyPilot/ClumsyPilot.csproj --configuration Debug
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_curve.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_curve.ps1
git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/ArcLengthCurvatureBezierCurve2D.cs ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_curve.ps1
git commit -m 'feat: add curvature-domain Local G2 probe curve'
Expected: build exit 0 and both script runs print Local G2 curvature probe curve checks passed. Record commands, exact output, baseline/head commits, and concerns in the Task 1 report.
Task 2: Implement the deterministic two-variable closure solver
Files:
- Create in disposable core:
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PoseClosureSolver.cs - Create in disposable core:
ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_solver.ps1 - Record without committing:
.superpowers/sdd/local-g2-excursion-task-2-report.md
Interfaces:
internal readonly struct LocalG2HardBoundaryState
{
internal LocalG2HardBoundaryState(double x, double y, double travelUnwrappedHeadingRadians,
double geometricCurvaturePerMeter, double referenceArcLengthMeters);
internal double X { get; }
internal double Y { get; }
internal double TravelUnwrappedHeadingRadians { get; }
internal double GeometricCurvaturePerMeter { get; }
internal double ReferenceArcLengthMeters { get; }
}
internal enum LocalG2ClosureFailure
{
None, InvalidInput, EmptyJointCurvatureDomain, IntegrationDepthLimit,
JacobianBoundaryDegenerate, SingularJacobian, NoDescentStep,
StalledAtBoundary, IterationLimit
}
internal sealed class LocalG2PoseClosureSolution
{
internal ArcLengthCurvatureBezierCurve2D Curve { get; }
internal int SeedIndex { get; }
internal int IterationCount { get; }
internal int DampingTrialCount { get; }
internal double LengthMeters { get; }
internal double C0 { get; }
internal double C1 { get; }
internal double C2 { get; }
internal double C3 { get; }
internal double PreOverwritePositionErrorMeters { get; }
internal double HeadingErrorRadians { get; }
}
internal sealed class LocalG2PoseClosureDiagnostic
{
internal int SeedIndex { get; }
internal int OuterIterationCount { get; }
internal int DampingTrialCount { get; }
internal int IntegrationEvaluationCount { get; }
internal int MaximumIntegrationDepth { get; }
internal double? LastLengthMeters { get; }
internal double? LastC1 { get; }
internal double? LastC2 { get; }
internal LocalG2ClosureFailure Failure { get; }
internal string Reason { get; }
}
internal sealed class LocalG2PoseClosureSolver
{
internal bool TrySolve(LocalG2HardBoundaryState start, LocalG2HardBoundaryState end,
double maximumDeviationMeters, double maximumVehicleCurvaturePerMeter,
int seedIndex, CancellationToken cancellationToken,
out LocalG2PoseClosureSolution solution,
out LocalG2PoseClosureDiagnostic diagnostic,
out LocalG2ClosureFailure failure, out string reason);
public sealed class PoseClosureTestSnapshot
{
public bool Accepted { get; }
public double PositionError { get; }
public double HeadingError { get; }
public double C1 { get; }
public double C2 { get; }
public bool ExactBounds { get; }
public bool EmptyDomainRejected { get; }
public bool C2NeverClamped { get; }
public bool LengthBoundsExact { get; }
public bool ChordAboveMaximumRejected { get; }
public bool DifferenceRulesExact { get; }
public bool ProjectionOrderExact { get; }
public bool FourTrialLimitExact { get; }
public bool DampingScheduleExact { get; }
public bool PivotThresholdExact { get; }
public bool EqualStepStalls { get; }
public bool AboveStepContinues { get; }
public bool IterationLimitExact { get; }
public bool FinalTrialClosureAccepted { get; }
public bool AllFailureExitsStable { get; }
public IReadOnlyList<string> FailureExitNames { get; }
public bool AllSeedsOrdered { get; }
public double SeedLowerBound { get; }
public double SeedUpperBound { get; }
public double Seed0C1 { get; }
public double Seed1C1 { get; }
public double Seed2C1 { get; }
public bool RepeatedSolutionsEqual { get; }
public bool CancellationPropagates { get; }
}
public static class TestHooks
{
public static PoseClosureTestSnapshot Execute(string scenario);
}
}
-
Consumes Task 1 curve.
-
Produces one interval solution; Task 3 owns window/seed scheduling and total invocation budgets.
-
Every non-cancelled
TrySolvecall returns a non-null immutable diagnostic, includingInvalidInputand all other failures. Fields that were never evaluated remain nullable; cancellation throws and does not fabricate a diagnostic. -
Step 1: Write solver RED tests
The reflection script invokes LocalG2PoseClosureSolver.TestHooks.Execute and checks:
$known = Invoke-Scenario 'KnownCurveRecovery'
Assert-True $known.Accepted 'A known cubic-curvature endpoint must be recovered.'
Assert-True ($known.PositionError -le 1e-9) 'Known curve position must close.'
Assert-True ($known.HeadingError -le 1e-8) 'Known curve heading must close.'
$counter = Invoke-Scenario 'SingleTurnLeftCounterCurvature'
Assert-True $counter.Accepted 'The zero-heading left interval must close within physical bounds.'
Assert-True ($counter.C1 -lt 0.0 -or $counter.C2 -lt 0.0) 'Closure must expose counter-curvature.'
$domain = Invoke-Scenario 'JointDomain'
Assert-True $domain.ExactBounds 'c1 bounds must equal max(-K,S-K)..min(K,S+K).'
Assert-True $domain.EmptyDomainRejected 'An empty joint domain must be rejected.'
Assert-True $domain.C2NeverClamped 'c2 must be derived, never independently clamped.'
$length = Invoke-Scenario 'LengthBounds'
Assert-True $length.LengthBoundsExact 'Length must be projected to [chord,rawSpan+2*deviation].'
Assert-True $length.ChordAboveMaximumRejected 'An empty length interval must reject InvalidInput.'
$algorithm = Invoke-Scenario 'AlgorithmConstants'
Assert-True $algorithm.DifferenceRulesExact 'Central/one-sided finite differences must match the design.'
Assert-True $algorithm.ProjectionOrderExact 'Projection order must match the design.'
Assert-True $algorithm.FourTrialLimitExact 'Each outer iteration must permit at most four damping trials.'
Assert-True $algorithm.DampingScheduleExact 'Lambda must start at 1e-3 and change by factors of ten.'
Assert-True $algorithm.PivotThresholdExact 'The 1e-14 pivot threshold must be enforced.'
Assert-True $algorithm.EqualStepStalls 'normalizedStep exactly 1e-12 must stall when not closed.'
Assert-True $algorithm.AboveStepContinues 'normalizedStep above 1e-12 must not take the stall exit.'
Assert-True $algorithm.IterationLimitExact 'The outer iteration cap must be 16.'
Assert-True $algorithm.FinalTrialClosureAccepted `
'A closing fourth trial in outer iteration 16 must succeed, not return IterationLimit.'
Assert-True $algorithm.AllFailureExitsStable `
'Every failure enum must be triggered by a deterministic fixture and retain its exact reason.'
Assert-Sequence @('InvalidInput','EmptyJointCurvatureDomain','IntegrationDepthLimit', `
'JacobianBoundaryDegenerate','SingularJacobian','NoDescentStep', `
'StalledAtBoundary','IterationLimit') $algorithm.FailureExitNames `
'Every non-cancellation solver failure exit must be covered exactly once.'
$stable = Invoke-Scenario 'DeterminismAndCancellation'
Assert-True $stable.AllSeedsOrdered 'Seeds must be mid, lower-quarter, upper-quarter.'
Assert-Near (($stable.SeedLowerBound + $stable.SeedUpperBound) / 2.0) `
$stable.Seed0C1 1e-15 'Seed 0 must be the exact midpoint.'
Assert-Near (($stable.SeedLowerBound + $stable.Seed0C1) / 2.0) `
$stable.Seed1C1 1e-15 'Seed 1 must be the exact lower quarter.'
Assert-Near (($stable.Seed0C1 + $stable.SeedUpperBound) / 2.0) `
$stable.Seed2C1 1e-15 'Seed 2 must be the exact upper quarter.'
Assert-True $stable.RepeatedSolutionsEqual 'Repeated solution fields must match exactly.'
Assert-True $stable.CancellationPropagates 'Cancellation must throw OperationCanceledException.'
- Step 2: Run RED
dotnet build ClumsyPilot/ClumsyPilot.csproj --configuration Debug
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_solver.ps1
Expected: reflection FAIL because the solver type does not exist.
- Step 3: Implement joint domain and analytic heading elimination
Use exactly:
double chord = Distance(start.X, start.Y, end.X, end.Y);
double rawSpan = end.ReferenceArcLengthMeters - start.ReferenceArcLengthMeters;
double maximumLength = rawSpan + 2d * maximumDeviationMeters;
double deltaHeading = end.TravelUnwrappedHeadingRadians -
start.TravelUnwrappedHeadingRadians;
double SumForLength(double length) =>
4d * deltaHeading / length - start.GeometricCurvaturePerMeter -
end.GeometricCurvaturePerMeter;
void GetJointBounds(double length, out double low, out double high)
{
double sum = SumForLength(length);
low = Math.Max(-maximumVehicleCurvaturePerMeter,
sum - maximumVehicleCurvaturePerMeter);
high = Math.Min(maximumVehicleCurvaturePerMeter,
sum + maximumVehicleCurvaturePerMeter);
}
After projecting L, recompute bounds, project c1, then derive c2=SumForLength(L)-c1. Never clamp c2.
- Step 4: Implement the exact finite-difference and LM contract
Bind these constants in one place:
private const int MaximumOuterIterations = 16;
private const int MaximumDampingTrials = 4;
private const double InitialDamping = 1e-3d;
private const double MinimumDamping = 1e-12d;
private const double MaximumDamping = 1e12d;
private const double PivotTolerance = 1e-14d;
private const double StalledStepTolerance = 1e-12d;
private const double PositionToleranceMeters = 1e-9d;
private const double HeadingToleranceRadians = 1e-8d;
Use:
double positionScale = Math.Max(rawSpan, 1e-3d);
double hL = Math.Max(1e-7d, 1e-6d * positionScale);
double hC = Math.Max(1e-8d, 1e-6d * maximumVehicleCurvaturePerMeter);
double stepL = Math.Abs(trialL - currentL) / positionScale;
double stepC = Math.Abs(trialC1 - currentC1) /
Math.Max(maximumVehicleCurvaturePerMeter, 1e-8d);
double normalizedStep = Math.Max(stepL, stepC);
J is the Jacobian of [dx/positionScale,dy/positionScale]. Prefer central differences; use the actual projected variable delta in the denominator; fall back to one-sided when only one side is legal. Solve (J^T J + lambda * diag(max(diag(J^T J),1))) * delta = -J^T rNormalized as a damped 2x2 normal equation with partial pivoting. Accept only a finite strict norm decrease. Four failed trials return NoDescentStep; do not hide trials inside the outer-iteration counter.
Implement TrySolve with this complete control flow:
- Check cancellation; validate all inputs,
seedIndex in [0,2],rawSpan>0,maximumDeviation>=0, andKvehicle>0. ComputeminimumLength=chord,maximumLength=rawSpan+2*maximumDeviation; returnInvalidInputwhen the interval is empty or non-finite. - Project
rawSpanto the length interval, compute the jointc1bounds, and returnEmptyJointCurvatureDomainwhen empty. Initializec1asmid,(low+mid)/2, or(mid+high)/2for seed 0, 1, or 2. Derivec2; evaluate the curve and normalized residual. Propagate cancellation and map only integration depth exhaustion toIntegrationDepthLimit. - At the start of each of 16 outer iterations, check cancellation and all success conditions: Euclidean position
<=1e-9, analytic heading<=1e-8, endpoint curvatures<=1e-8, finite values, and all four controls inside the physical domain. Return the immutable solution immediately only when every condition passes. - For
Lthenc1, form projected plus/minus perturbations in that order. Evaluate both distinct legal sides for a central column; otherwise evaluate the one distinct legal side against the current residual. Divide by the actual projected physical-variable delta. If neither side is distinct/legal, returnJacobianBoundaryDegenerate. - Carry
lambdaacross outer iterations, initially1e-3. For damping trials 0 through 3: check cancellation; form the exact damped normal equation; solve it with partial pivoting; returnSingularJacobianwhen either selected absolute pivot is<1e-14; project the proposedL, recompute/project the jointc1domain, derivec2, and evaluate the trial. - Immediately after every trial evaluation and before testing step size or descent, run the complete hard-success predicate from item 3 and return the immutable solution if it passes. Otherwise, if projected
normalizedStep<=1e-12, returnStalledAtBoundary. Accept only a finite strict residual-norm decrease, setlambda=max(1e-12,lambda/10), store the trial, and begin the next outer iteration. On rejection setlambda=min(1e12,lambda*10)and try again. Four rejected trials returnNoDescentStep. This post-trial success check also applies to the fourth damping trial of the 16th outer iteration; a closed final trial must never fall through toIterationLimit. - After the 16th unsuccessful outer iteration return
IterationLimit. Never catchOperationCanceledException, randomize seeds, use wall-clock termination, or reuse damping/history across calls.
- Step 5: Run GREEN twice, run Task 1 regression, and commit
dotnet build ClumsyPilot/ClumsyPilot.csproj --configuration Debug
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_solver.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_solver.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_curve.ps1
git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PoseClosureSolver.cs ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_solver.ps1
git commit -m 'feat: add bounded Local G2 pose closure probe'
Expected: all scripts exit 0 with their named pass messages. Record the exact solver status counts from synthetic cases.
Task 3: Integrate bounded candidates and emit strict candidate audit data
Files:
- Create in disposable core:
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2ExcursionProbeDiagnostics.cs - Modify in disposable core:
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs - Modify in disposable core:
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs - Modify in disposable core:
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs - Create in disposable core:
ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1 - Record without committing:
.superpowers/sdd/local-g2-excursion-task-3-report.md
Interfaces:
internal sealed class LocalG2ExcursionIntervalDiagnostic
{
internal int IntervalIndex { get; }
internal bool Accepted { get; }
internal ArcLengthCurvatureBezierCurve2D Curve { get; }
internal LocalG2ClosureFailure Failure { get; }
internal string Reason { get; }
internal double? LengthMeters { get; }
internal double? GeometricC0 { get; }
internal double? GeometricC1 { get; }
internal double? GeometricC2 { get; }
internal double? GeometricC3 { get; }
internal double? VehicleC0 { get; }
internal double? VehicleC1 { get; }
internal double? VehicleC2 { get; }
internal double? VehicleC3 { get; }
internal int OuterIterationCount { get; }
internal int DampingTrialCount { get; }
internal int IntegrationEvaluationCount { get; }
internal int MaximumIntegrationDepth { get; }
internal long SolverElapsedStopwatchTicks { get; }
internal long StopwatchFrequency { get; }
internal double? PreOverwritePositionErrorMeters { get; }
internal double? AnalyticHeadingErrorRadians { get; }
internal double? AnalyticMinimumCurvaturePerMeter { get; }
internal double? AnalyticMaximumCurvaturePerMeter { get; }
internal double? AnalyticMaximumGradientPerSquareMeter { get; }
}
internal sealed class LocalG2ExcursionCandidateDiagnostic
{
internal int WindowIndex { get; }
internal int SeedIndex { get; }
internal int CombinationAttemptIndex { get; }
internal int SolverInvocationCount { get; }
internal int DirectionSign { get; }
internal IReadOnlyList<LocalG2HardBoundaryState> HardNodes { get; }
internal IReadOnlyList<LocalG2ExcursionIntervalDiagnostic> Intervals { get; }
internal double MaximumPreOverwritePositionErrorMeters { get; }
internal double MaximumPostOverwritePositionErrorMeters { get; }
internal double MaximumTangentSeamErrorRadians { get; }
internal double MaximumCurvatureSeamErrorPerMeter { get; }
internal double MinimumAdjacentPointDistanceMeters { get; }
}
internal sealed class LocalG2ExcursionCandidateLedgerEntry
{
internal int WindowIndex { get; }
internal int SeedIndex { get; }
internal int CombinationAttemptIndex { get; }
internal LocalG2CandidateGeometry Candidate { get; }
internal bool IsCanonical { get; }
internal int CanonicalCandidateIndex { get; }
internal IReadOnlyList<double> EquivalenceParameters { get; }
internal double MaximumEquivalenceCoordinateDifferenceMeters { get; }
}
internal enum LocalG2ExcursionAttemptOutcome
{
CandidateConstructed,
NumericallyDeduplicated,
HardNodeInvalid,
IntervalSolveFailed,
CandidateAssemblyFailed,
CombinationBudgetExhausted,
SolverInvocationBudgetExhausted,
CandidateLimitReached,
ProbeInvalid
}
internal enum LocalG2ExcursionSearchStop
{
None,
CombinationBudgetExhausted,
SolverInvocationBudgetExhausted,
CandidateLimitReached,
ProbeInvalid
}
internal sealed class LocalG2ExcursionAttemptDiagnostic
{
internal int WindowIndex { get; }
internal int SeedIndex { get; }
internal int CombinationAttemptIndex { get; }
internal int IntervalIndex { get; }
internal int EquivalentCandidateIndex { get; }
internal LocalG2ExcursionAttemptOutcome Outcome { get; }
internal LocalG2ClosureFailure ClosureFailure { get; }
internal string Reason { get; }
internal IReadOnlyList<LocalG2ExcursionIntervalDiagnostic> Intervals { get; }
}
internal sealed class LocalG2ExcursionProbeRunDiagnostic
{
internal int WindowCount { get; }
internal int CombinationAttemptCount { get; }
internal int FailedCombinationCount { get; }
internal int SolverInvocationCount { get; }
internal int CandidateCount { get; }
internal int MaximumIntervalsPerCombination { get; }
internal LocalG2ExcursionSearchStop SearchStop { get; }
internal int SearchStopWindowIndex { get; }
internal int SearchStopSeedIndex { get; }
internal bool ProbeInvalid { get; }
internal string ProbeInvalidReason { get; }
internal bool HasCandidateGeometryMetrics { get; }
internal double? MaximumPreOverwritePositionErrorMeters { get; }
internal double? MaximumPostOverwritePositionErrorMeters { get; }
internal double? MaximumTangentSeamErrorRadians { get; }
internal double? MaximumCurvatureSeamErrorPerMeter { get; }
internal double? MinimumAdjacentPointDistanceMeters { get; }
internal IReadOnlyList<LocalG2ExcursionAttemptDiagnostic> Attempts { get; }
internal IReadOnlyList<LocalG2ExcursionCandidateLedgerEntry> CandidateLedger { get; }
}
internal sealed class LocalG2CandidateBuilder
{
private static readonly double[] EquivalenceParameters =
{ 0d, 0.25d, 0.5d, 0.75d, 1d };
internal LocalG2ExcursionProbeRunDiagnostic LastProbeDiagnostic { get; }
public sealed class DirectionRoundTripSnapshot
{
public double ForwardVehicleCurvature { get; }
public double ForwardGeometricCurvature { get; }
public double ReverseVehicleCurvature { get; }
public double ReverseGeometricCurvature { get; }
public bool ForwardHeadingRoundTrips { get; }
public bool ReverseHeadingRoundTrips { get; }
}
public sealed class EquivalenceToleranceSnapshot
{
public bool ExactThresholdsEquivalent { get; }
public bool AboveAnyThresholdDistinct { get; }
public bool HardNodeMismatchDistinct { get; }
public bool IntervalCountMismatchDistinct { get; }
public bool FixedEquivalenceGridExact { get; }
public bool AliasRetainsIndependentDigestInput { get; }
}
public static class TestHooks
{
public static DirectionRoundTripSnapshot ExecuteDirectionRoundTrip();
public static EquivalenceToleranceSnapshot ExecuteEquivalenceTolerance();
}
}
internal IReadOnlyList<LocalG2CandidateGeometry> Build(
PreparedDirectionSegment originalSegment,
LocalG2SmoothingRegion region,
double outputSpacingMeters,
double maximumVehicleCurvaturePerMeter,
LocalG2OptionsSnapshot options,
CancellationToken cancellationToken);
private bool TryCreateHardBoundaryStates(
PreparedDirectionSegment originalSegment,
LocalG2SmoothingRegion region,
LocalG2WindowVariant window,
double maximumVehicleCurvaturePerMeter,
out IReadOnlyList<LocalG2HardBoundaryState> hardNodes,
out string reason);
private bool TryCreateCandidateFromSolutions(
PreparedDirectionSegment originalSegment,
LocalG2SmoothingRegion region,
LocalG2WindowVariant window,
int windowIndex,
int seedIndex,
int combinationAttemptIndex,
int candidateIndex,
IReadOnlyList<LocalG2HardBoundaryState> hardNodes,
IReadOnlyList<LocalG2PoseClosureSolution> solutions,
IReadOnlyList<LocalG2ExcursionIntervalDiagnostic> intervalDiagnostics,
double outputSpacingMeters,
int solverInvocationCount,
out LocalG2CandidateGeometry candidate,
out LocalG2ExcursionCandidateDiagnostic diagnostic,
out string reason);
private void RecordAttemptFailure(int windowIndex, int seedIndex,
int combinationAttemptIndex, LocalG2ClosureFailure failure, string reason);
private void RecordIntervalFailure(int windowIndex, int seedIndex,
int combinationAttemptIndex, int intervalIndex,
LocalG2ClosureFailure failure, string reason,
IReadOnlyList<LocalG2ExcursionIntervalDiagnostic> intervalDiagnostics);
private void RecordCandidateFailure(int windowIndex, int seedIndex,
int combinationAttemptIndex, LocalG2ExcursionCandidateDiagnostic diagnostic,
string reason);
private void MarkProbeInvalid(int windowIndex, int seedIndex, string reason);
private void MarkSearchStopped(int windowIndex, int seedIndex,
LocalG2ExcursionSearchStop stop, string reason);
private bool TryAddIfNumericallyNew(List<LocalG2CandidateGeometry> candidates,
LocalG2CandidateGeometry candidate,
LocalG2ExcursionCandidateDiagnostic diagnostic,
CancellationToken cancellationToken, out string reason);
private static bool TryAreNumericallyEquivalent(
LocalG2ExcursionCandidateDiagnostic left,
LocalG2ExcursionCandidateDiagnostic right,
CancellationToken cancellationToken, out bool equivalent,
out double maximumCoordinateDifferenceMeters, out string reason);
private static LocalG2ExcursionIntervalDiagnostic CreateIntervalDiagnostic(
int intervalIndex, int directionSign,
LocalG2HardBoundaryState start, LocalG2HardBoundaryState end,
LocalG2PoseClosureSolution solution,
LocalG2PoseClosureDiagnostic solverDiagnostic,
LocalG2ClosureFailure failure, string reason,
long solverElapsedStopwatchTicks, long stopwatchFrequency);
private static int DirectionSign(TravelDirection direction);
The displayed builder TestHooks methods are added to the existing nested TestHooks class; do not create a second class, replace the existing Execute(string): CandidateTestSnapshot, or overload by return type.
- The
LocalG2CandidateGeometryconstructor adds one final optional parameter:
LocalG2ExcursionCandidateDiagnostic excursionDiagnostic = null
- The probe script command-line contract is:
param(
[ValidateSet('Strict','Measurement','FinalGate')]
[string]$Mode = 'Strict',
[string]$StrictRoot,
[string]$MeasurementRoot,
[string]$EvidenceRoot,
[string]$RunLabel = 'single',
[string]$CoreCommit,
[switch]$Batch
)
Strict and non-batch Measurement use the assembly under the current root and emit one run at the path returned by Join-Path $EvidenceRoot ($RunLabel + '.json'); when EvidenceRoot is omitted for a one-root developer run, create a unique directory under [IO.Path]::GetTempPath() and print its full path. Strict may omit CoreCommit, in which case both identity commits are the current root HEAD; when supplied it must equal HEAD. Measurement requires CoreCommit, records its current HEAD as RootCommit, and requires every committed path in CoreCommit..HEAD to be exactly the evaluator path. Measurement -Batch loads the assembly and fixture once, performs five warm-up runs followed immediately by 30 measured runs serially in that same process, and emits one batch JSON containing both arrays. Batch is invalid with Strict or FinalGate. FinalGate requires both absolute root paths and an explicit EvidenceRoot, derives the expected core commit from strict-root HEAD, launches one strict child and one measurement-batch child with that exact CoreCommit, pairs their evidence, writes timing/state/report artifacts, and exits nonzero only for PROBE_INVALID or a broken test contract; valid RED/STRICT_GREEN/FEASIBLE states are evidence outcomes and exit zero.
- Step 1: Write the strict-root candidate/audit RED seam
The new script must use the real single-turn fixture and existing preprocessor/detector/window planner. Its Invoke-ProbeHook helper maps DirectionRoundTrip to LocalG2CandidateBuilder.TestHooks.ExecuteDirectionRoundTrip() and EquivalenceTolerance to ExecuteEquivalenceTolerance(), and throws for any other name or absent snapshot. Before evaluation it asserts exact attempt limits and hard geometry diagnostics:
$runDiagnostic = Get-InternalProperty $builder 'LastProbeDiagnostic'
$attempts = @(Get-InternalProperty $runDiagnostic 'Attempts')
$candidateLedger = @(Get-InternalProperty $runDiagnostic 'CandidateLedger')
$allIntervals = @($attempts | ForEach-Object {
@(Get-InternalProperty $_ 'Intervals')
})
$hasCandidateGeometryMetrics = [bool](Get-InternalProperty $runDiagnostic 'HasCandidateGeometryMetrics')
$probe = [pscustomobject]@{
WindowCount = [int](Get-InternalProperty $runDiagnostic 'WindowCount')
CombinationAttemptCount = [int](Get-InternalProperty $runDiagnostic 'CombinationAttemptCount')
SolverInvocationCount = [int](Get-InternalProperty $runDiagnostic 'SolverInvocationCount')
MaximumIntervalsPerCombination = [int](Get-InternalProperty $runDiagnostic 'MaximumIntervalsPerCombination')
CandidateCount = [int](Get-InternalProperty $runDiagnostic 'CandidateCount')
CandidateLedgerCount = $candidateLedger.Count
CanonicalLedgerCount = @($candidateLedger | Where-Object {
[bool](Get-InternalProperty $_ 'IsCanonical')
}).Count
DeduplicatedLedgerCount = @($candidateLedger | Where-Object {
-not [bool](Get-InternalProperty $_ 'IsCanonical')
}).Count
ProbeInvalid = [bool](Get-InternalProperty $runDiagnostic 'ProbeInvalid')
SearchStop = (Get-InternalProperty $runDiagnostic 'SearchStop').ToString()
HasCandidateGeometryMetrics = $hasCandidateGeometryMetrics
MaximumPreOverwritePositionError = $(if ($hasCandidateGeometryMetrics) { [double](Get-InternalProperty $runDiagnostic 'MaximumPreOverwritePositionErrorMeters') } else { $null })
MaximumPostOverwritePositionError = $(if ($hasCandidateGeometryMetrics) { [double](Get-InternalProperty $runDiagnostic 'MaximumPostOverwritePositionErrorMeters') } else { $null })
MaximumTangentError = $(if ($hasCandidateGeometryMetrics) { [double](Get-InternalProperty $runDiagnostic 'MaximumTangentSeamErrorRadians') } else { $null })
MaximumCurvatureError = $(if ($hasCandidateGeometryMetrics) { [double](Get-InternalProperty $runDiagnostic 'MaximumCurvatureSeamErrorPerMeter') } else { $null })
MinimumAdjacentDistance = $(if ($hasCandidateGeometryMetrics) { [double](Get-InternalProperty $runDiagnostic 'MinimumAdjacentPointDistanceMeters') } else { $null })
AllAttemptsCategorized = Test-AllAttemptsCategorized $attempts `
([int](Get-InternalProperty $runDiagnostic 'CombinationAttemptCount'))
IntervalDiagnosticCount = $allIntervals.Count
AllSolverTicksNonnegative = -not @($allIntervals | Where-Object {
[long](Get-InternalProperty $_ 'SolverElapsedStopwatchTicks') -lt 0
}).Count
AllStopwatchFrequenciesPositive = -not @($allIntervals | Where-Object {
[long](Get-InternalProperty $_ 'StopwatchFrequency') -le 0
}).Count
StrictEvaluatorSourceMatchesBaseline = Test-StrictEvaluatorHash
CandidateLedgerAuditValid =
Test-CandidateDigestDeduplication $candidateLedger $attempts
}
Assert-True ($probe.WindowCount -le 12) 'Window attempts must be capped at 12.'
Assert-True ($probe.CombinationAttemptCount -le 36) 'Combinations must be capped at 36.'
Assert-True ($probe.SolverInvocationCount -le 96) 'Failed solver calls must count toward 96.'
Assert-True ($probe.IntervalDiagnosticCount -eq $probe.SolverInvocationCount) `
'Every completed solver invocation must emit one timing diagnostic.'
Assert-True $probe.AllSolverTicksNonnegative 'Solver elapsed ticks must be nonnegative.'
Assert-True $probe.AllStopwatchFrequenciesPositive 'Stopwatch frequencies must be positive.'
if ($probe.MaximumIntervalsPerCombination -gt 8) {
Assert-True $probe.ProbeInvalid 'More than 8 intervals must produce PROBE_INVALID.'
Assert-True ($probe.SearchStop -eq 'ProbeInvalid') 'Invalid interval count must stop all scheduling.'
} else {
Assert-True ($probe.MaximumIntervalsPerCombination -le 8) 'A scheduled combination must contain at most 8 intervals.'
}
Assert-True ($probe.CandidateCount -le 12) 'Constructed candidates must be capped at 12.'
Assert-True ($probe.CandidateCount -eq $probe.CanonicalLedgerCount) `
'CandidateCount must count canonical ledger entries only.'
Assert-True ($probe.CandidateLedgerCount -eq
($probe.CanonicalLedgerCount + $probe.DeduplicatedLedgerCount)) `
'Every ledger record must be canonical or a deduplicated alias.'
Assert-True $probe.AllAttemptsCategorized 'Every failed, exhausted, invalid, constructed, or deduplicated attempt must be retained.'
if ($probe.CandidateCount -gt 0) {
Assert-True ($probe.MaximumPreOverwritePositionError -le 1e-9) 'Pre-overwrite closure must pass.'
Assert-True ($probe.MaximumPostOverwritePositionError -le 1e-9) 'Post-overwrite anchor must pass.'
Assert-True ($probe.MaximumTangentError -le 1e-8) 'Travel tangent seams must be G2.'
Assert-True ($probe.MaximumCurvatureError -le 1e-8) 'Curvature seams must be G2.'
Assert-True ($probe.MinimumAdjacentDistance -gt 1e-10) 'Candidate points must not repeat.'
} else {
Assert-True (-not $probe.HasCandidateGeometryMetrics) `
'No-candidate runs must not synthesize hard-geometry metrics.'
}
Assert-True $probe.StrictEvaluatorSourceMatchesBaseline 'Strict evaluator must remain unchanged.'
Assert-True $probe.CandidateLedgerAuditValid `
'Candidate ledger keys, digests, canonical links, and deduplication evidence must be valid.'
$direction = Invoke-ProbeHook 'DirectionRoundTrip'
Assert-Near $direction.ForwardVehicleCurvature $direction.ForwardGeometricCurvature 1e-12 `
'Forward geometric curvature must preserve the vehicle sign.'
Assert-Near (-$direction.ReverseVehicleCurvature) $direction.ReverseGeometricCurvature 1e-12 `
'Reverse geometric curvature must negate the vehicle sign.'
Assert-True $direction.ForwardHeadingRoundTrips 'Forward travel/vehicle heading conversion must round-trip.'
Assert-True $direction.ReverseHeadingRoundTrips 'Reverse travel/vehicle heading conversion must round-trip.'
$equivalence = Invoke-ProbeHook 'EquivalenceTolerance'
Assert-True $equivalence.ExactThresholdsEquivalent `
'Equal 1e-9 L/c1/c2 and coordinate differences must deduplicate.'
Assert-True $equivalence.AboveAnyThresholdDistinct `
'A difference above any 1e-9 threshold must remain distinct.'
Assert-True $equivalence.HardNodeMismatchDistinct `
'Different hard-node sequences must remain distinct.'
Assert-True $equivalence.IntervalCountMismatchDistinct `
'Different interval counts must remain distinct.'
Assert-True $equivalence.FixedEquivalenceGridExact `
'The common grid must be exactly 0, 0.25, 0.5, 0.75, 1 per interval.'
Assert-True $equivalence.AliasRetainsIndependentDigestInput `
'A deduplicated key must retain its own hard nodes and candidate points.'
Test-AllAttemptsCategorized requires exactly one terminal attempt record for every started combination index 1..CombinationAttemptCount; permitted terminal outcomes are constructed, deduplicated, hard-node invalid, interval failure, candidate assembly failure, started-combination budget exhaustion, or probe invalid. Test-StrictEvaluatorHash compares the current evaluator SHA-256 with the Task 1 manifest. Test-CandidateDigestDeduplication($candidateLedger,$attempts) recomputes a digest from every entry's own retained candidate, requires one ledger entry for every CandidateConstructed or NumericallyDeduplicated terminal attempt and none for failed attempts, requires stable keys to be unique and canonical indices to be unique/dense, and requires each duplicate attempt and alias to name the same preserved canonical index. Equal content SHA-256 values on two canonical records do not themselves deduplicate or invalidate them; only the fixed continuous-curve equivalence contract may deduplicate. Each alias must keep a distinct stable key, its own digest input/digest, the exact fixed grid and measured proof, and a resolvable canonical evaluation source; the script separately constructs evaluator-call, eligibility, state-count, performance-count, and selection input arrays and asserts no alias stable key occurs in any of them.
The script must emit one invariant JSON object per attempt/candidate, including stable key, digest, interval values, failures, strict result, actual strict stop gate, raw/candidate ranges when evaluated, and not evaluated for gates after strict early return.
- Step 2: Run RED
dotnet build ClumsyPilot/ClumsyPilot.csproj --configuration Debug
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1 -Mode Strict
Expected: FAIL because the builder signature, diagnostics, and curvature-domain scheduling do not exist.
- Step 3: Add immutable probe diagnostics
Diagnostics validate all finite/nonnegative fields in constructors and copy input lists into ReadOnlyCollection<T>. Store both geometric and recovered vehicle c0..c3, L, seed, iteration/damping/integration counts, failure enum, pre/post errors, analytic extrema, and seam errors. EquivalentCandidateIndex is the dense emitted index only for NumericallyDeduplicated and is -1 for every other outcome. Do not add public production contracts; these files exist only in the disposable repository.
- Step 4: Replace candidate scheduling in the disposable builder
Add using System.Diagnostics; and use this exact outer scheduling shape:
int maximumWindows = Math.Min(region.WindowVariants.Count, 12);
int combinationAttempts = 0;
int solverInvocations = 0;
bool stopScheduling = false;
for (int windowIndex = 0; windowIndex < maximumWindows && !stopScheduling; windowIndex++)
{
LocalG2WindowVariant window = region.WindowVariants[windowIndex];
for (int seedIndex = 0; seedIndex < 3; seedIndex++)
{
cancellationToken.ThrowIfCancellationRequested();
if (candidates.Count >= Math.Min(options.MaximumCandidatesPerRegion, 12))
{
MarkSearchStopped(windowIndex, seedIndex,
LocalG2ExcursionSearchStop.CandidateLimitReached,
"The constructed-candidate limit was reached.");
stopScheduling = true;
break;
}
if (combinationAttempts >= 36)
{
MarkSearchStopped(windowIndex, seedIndex,
LocalG2ExcursionSearchStop.CombinationBudgetExhausted,
"The window/seed combination limit was reached.");
stopScheduling = true;
break;
}
if (solverInvocations >= 96)
{
MarkSearchStopped(windowIndex, seedIndex,
LocalG2ExcursionSearchStop.SolverInvocationBudgetExhausted,
"The solver invocation limit was reached.");
stopScheduling = true;
break;
}
combinationAttempts++;
if (!TryCreateHardBoundaryStates(originalSegment, region, window,
maximumVehicleCurvaturePerMeter,
out IReadOnlyList<LocalG2HardBoundaryState> hardNodes,
out string hardNodeReason))
{
RecordAttemptFailure(windowIndex, seedIndex, combinationAttempts,
LocalG2ClosureFailure.InvalidInput, hardNodeReason);
continue;
}
int intervalCount = hardNodes.Count - 1;
if (intervalCount < 1 || intervalCount > 8)
{
MarkProbeInvalid(windowIndex, seedIndex,
"Hard-node interval count must be in [1,8].");
stopScheduling = true;
break;
}
if (solverInvocations + intervalCount > 96)
{
MarkSearchStopped(windowIndex, seedIndex,
LocalG2ExcursionSearchStop.SolverInvocationBudgetExhausted,
"The remaining solver budget cannot cover this combination.");
stopScheduling = true;
break;
}
var solutions = new List<LocalG2PoseClosureSolution>(intervalCount);
var intervalDiagnostics = new List<LocalG2ExcursionIntervalDiagnostic>(intervalCount);
bool solved = true;
for (int intervalIndex = 0; intervalIndex < intervalCount; intervalIndex++)
{
solverInvocations++;
long solverStarted = Stopwatch.GetTimestamp();
bool intervalAccepted = _poseClosureSolver.TrySolve(
hardNodes[intervalIndex], hardNodes[intervalIndex + 1],
options.MaximumDeviationMeters, maximumVehicleCurvaturePerMeter,
seedIndex, cancellationToken, out LocalG2PoseClosureSolution solution,
out LocalG2PoseClosureDiagnostic solverDiagnostic,
out LocalG2ClosureFailure failure, out string solveReason);
long solverElapsed = Stopwatch.GetTimestamp() - solverStarted;
LocalG2ExcursionIntervalDiagnostic intervalDiagnostic =
CreateIntervalDiagnostic(intervalIndex, DirectionSign(originalSegment.Direction),
hardNodes[intervalIndex], hardNodes[intervalIndex + 1],
solution, solverDiagnostic, failure, solveReason,
solverElapsed, Stopwatch.Frequency);
intervalDiagnostics.Add(intervalDiagnostic);
if (!intervalAccepted)
{
RecordIntervalFailure(windowIndex, seedIndex, combinationAttempts,
intervalIndex, failure, solveReason, intervalDiagnostics);
solved = false;
break;
}
solutions.Add(solution);
}
if (!solved) continue;
if (TryCreateCandidateFromSolutions(originalSegment, region, window,
windowIndex, seedIndex, combinationAttempts, candidates.Count,
hardNodes, solutions, intervalDiagnostics,
outputSpacingMeters, solverInvocations,
out LocalG2CandidateGeometry candidate,
out LocalG2ExcursionCandidateDiagnostic diagnostic,
out string candidateReason))
{
if (!TryAddIfNumericallyNew(candidates, candidate, diagnostic,
cancellationToken, out string equivalenceReason))
{
MarkProbeInvalid(windowIndex, seedIndex, equivalenceReason);
stopScheduling = true;
break;
}
}
else
{
RecordCandidateFailure(windowIndex, seedIndex, combinationAttempts,
diagnostic, candidateReason);
}
}
}
The method publishes LastProbeDiagnostic from one finally-style exit path after both loops. RecordAttemptFailure, MarkProbeInvalid, MarkSearchStopped, RecordIntervalFailure, RecordCandidateFailure, and TryAddIfNumericallyNew populate that immutable snapshot; none may silently discard a failed attempt. TryAddIfNumericallyNew applies the Task 3 equivalence contract below, preserves the first numerical solution, records later equivalent stable keys as deduplicated ledger entries, and keeps emitted candidateIndex values dense in combination order.
DirectionSign returns +1 for TravelDirection.Forward, -1 for TravelDirection.Reverse, and throws ArgumentOutOfRangeException otherwise. Every completed solver invocation appends its interval diagnostic before success/failure branching; a failed combination therefore retains all successful prefix intervals plus the failed interval, including elapsed ticks and exact exit diagnostics. SolverPhaseElapsedMilliseconds is the sum of SolverElapsedStopwatchTicks * 1000.0 / StopwatchFrequency over all invocation diagnostics, not a second independently rounded measurement.
Implement numerical equivalence here with the immutable common grid u={0,0.25,0.5,0.75,1} for every corresponding interval. Require identical hard-node count/content and interval count; for every interval require |delta L|, |delta c1|, and |delta c2| each <=1e-9; then evaluate both stored immutable curves at all five identical u values and require maximum Euclidean coordinate difference <=1e-9 m. Check cancellation before every interval and grid point. Equality at the threshold deduplicates; any field above it remains distinct. A curve-evaluation/integration failure returns false with a reason and makes the run PROBE_INVALID, not “distinct.” The Task 3 EquivalenceTolerance hook asserts the exact grid and boundary behavior.
Every successfully assembled combination is appended to CandidateLedger before canonical/deduplicated branching. A canonical entry retains its full candidate and has IsCanonical=true, CanonicalCandidateIndex=candidate.CandidateIndex, grid {0,0.25,0.5,0.75,1}, and maximum equivalence coordinate difference 0. A deduplicated entry retains its own full candidate points/hard nodes and independent digest input, has IsCanonical=false, points to the preserved canonical dense index, and stores the measured maximum grid difference. The builder returns only canonical candidates to the pipeline; ledger aliases do not increment CandidateCount.
Keep the current hard-node construction, interpolation, direction-sign handling, distance-weighted shared curvature, reference body-clearance lookup, source marker, and dense candidate indices. Convert each normalized vehicle heading to normalized travel heading. Set the first unwrapped travel heading to that normalized value; for every later node use currentUnwrapped = previousUnwrapped + AngleMath.ShortestSignedDifference(AngleMath.NormalizeRadians(previousUnwrapped), currentTravelNormalized). Pass those unwrapped travel headings to the solver. For each solved interval, sample count=max(1,ceil(L/outputSpacingMeters)); emit the shared hard node once; use exact anchors only after pre-overwrite closure passes.
- Step 5: Pass the physical curvature limit from the pipeline
Change the sole production-path call in the disposable LocalG2PreSmoothingPipeline to:
IReadOnlyList<LocalG2CandidateGeometry> candidates = _builder.Build(
preparedPath.Segments[region.SegmentIndex], region,
request.Configuration.OutputSpacingMeters, maximumCurvature,
options, cancellationToken);
Update disposable test-hook/reflection calls with explicit finite positive curvature limits. Do not modify LocalG2CandidateEvaluator.cs in the core.
- Step 6: Implement the complete audit/final-gate script in the disposable core
Implement all three command modes, the Task 4 child JSON schema, child-process pairing, timing capture, state resolver/table tests, and report generation now; Task 3 requires only Strict to pass, while Measurement intentionally remains RED until the isolated evaluator patch. The candidate digest input uses UTF-8 invariant round-trip text, one field per line, in this exact order:
The script owns one Get-ProbeSourceManifest -Root <absolute-root> helper. It enumerates the current filesystem only beneath explicit safe paths: root .gitattributes, ClumsyPilot/ClumsyPilot.csproj, every .cs file below ClumsyPilot/ParkrobTrajplanner/PathSmoothing except ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs, the fixture ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/path-smoothing-fixtures.json, and exactly the three probe verifier scripts created by Tasks 1–3. This allows Task 3's pre-commit GREEN run to hash its TDD worktree; the frozen post-commit run and both Task 4 roots additionally require clean Git status, so their manifest bytes are bound to the recorded commits. The helper rejects any path containing a tab/newline or auto_avoidance, requires all seven categories to be nonempty, requires .gitattributes raw text to be exactly * -text plus one LF, normalizes separators to /, sorts with StringComparer.Ordinal, and hashes each file's raw bytes with lowercase SHA-256. The canonical manifest text is one relative/path<TAB>lowercase-sha256<LF> line per entry including a final LF, encoded as UTF-8 without BOM; ProbeSourceManifestSha256 is the lowercase SHA-256 of those exact bytes. The evaluator is intentionally excluded because its strict/measurement hashes and complete diff are audited separately. Every single/batch child recomputes this manifest from its own root; when EvidenceRoot is supplied it also writes the canonical text to <RunLabel>-probe-source-manifest.txt and records that full path beside the hash.
Use this exact canonicalization core inside the helper:
$paths = [string[]]@($sourcePaths | ForEach-Object { $_.Replace('\','/') })
[Array]::Sort($paths, [StringComparer]::Ordinal)
$entries = foreach ($relativePath in $paths) {
if ($relativePath.IndexOf("`t", [StringComparison]::Ordinal) -ge 0 -or
$relativePath.IndexOf("`n", [StringComparison]::Ordinal) -ge 0 -or
$relativePath.IndexOf('auto_avoidance', [StringComparison]::Ordinal) -ge 0) {
throw "Illegal probe-manifest path: $relativePath"
}
$absolutePath = Join-Path $Root ($relativePath.Replace('/', [IO.Path]::DirectorySeparatorChar))
$fileSha = (Get-FileHash -Algorithm SHA256 -LiteralPath $absolutePath).Hash.ToLowerInvariant()
$relativePath + "`t" + $fileSha
}
$canonicalText = [string]::Join("`n", [string[]]$entries) + "`n"
$utf8NoBom = New-Object Text.UTF8Encoding($false)
$manifestBytes = $utf8NoBom.GetBytes($canonicalText)
$sha256 = [Security.Cryptography.SHA256]::Create()
try { $manifestSha256 = -join ($sha256.ComputeHash($manifestBytes) | ForEach-Object { $_.ToString('x2') }) }
finally { $sha256.Dispose() }
$sourcePaths is built from the exact six explicit files plus a recursive *.cs enumeration rooted only at ClumsyPilot/ParkrobTrajplanner/PathSmoothing; it never enumerates the ParkrobTrajplanner parent. Convert every absolute result back to a root-relative path and apply the exact evaluator exclusion before canonicalization. A missing file, duplicate normalized path, empty manifest, category-count failure, EOL-policy mismatch, core.autocrlf other than false, or final-evidence dirty Git status aborts the run as PROBE_INVALID evidence.
function Get-CandidateLedgerDigest {
param($ledgerEntry, $preparedSegment)
$candidate = Get-InternalProperty $ledgerEntry 'Candidate'
$culture = [Globalization.CultureInfo]::InvariantCulture
$builder = [Text.StringBuilder]::new()
[void]$builder.AppendLine(([int]$preparedSegment.Direction).ToString($culture))
[void]$builder.AppendLine(([int](Get-InternalProperty $candidate 'SegmentIndex')).ToString($culture))
[void]$builder.AppendLine(([double](Get-InternalProperty $candidate 'StartArcLengthMeters')).ToString('R', $culture))
[void]$builder.AppendLine(([double](Get-InternalProperty $candidate 'EndArcLengthMeters')).ToString('R', $culture))
$diagnostic = Get-InternalProperty $candidate 'ExcursionDiagnostic'
$directionSign = [int](Get-InternalProperty $diagnostic 'DirectionSign')
if ($directionSign -ne 1 -and $directionSign -ne -1) { throw 'Invalid direction sign.' }
$hardNodes = @(Get-InternalProperty $diagnostic 'HardNodes')
[void]$builder.AppendLine($hardNodes.Count.ToString($culture))
foreach ($node in $hardNodes) {
[void]$builder.AppendLine(([double](Get-InternalProperty $node 'X')).ToString('R', $culture))
[void]$builder.AppendLine(([double](Get-InternalProperty $node 'Y')).ToString('R', $culture))
[void]$builder.AppendLine(([double](Get-InternalProperty $node 'ReferenceArcLengthMeters')).ToString('R', $culture))
[void]$builder.AppendLine(([double](Get-InternalProperty $node 'TravelUnwrappedHeadingRadians')).ToString('R', $culture))
$geometricCurvature = [double](Get-InternalProperty $node 'GeometricCurvaturePerMeter')
[void]$builder.AppendLine($geometricCurvature.ToString('R', $culture))
[void]$builder.AppendLine(($directionSign * $geometricCurvature).ToString('R', $culture))
}
$regionPoints = @(Get-InternalProperty $candidate 'RegionPoints')
[void]$builder.AppendLine($regionPoints.Count.ToString($culture))
foreach ($point in $regionPoints) {
[void]$builder.AppendLine(([double](Get-InternalProperty $point 'X')).ToString('R', $culture))
[void]$builder.AppendLine(([double](Get-InternalProperty $point 'Y')).ToString('R', $culture))
[void]$builder.AppendLine(([double](Get-InternalProperty $point 'ArcLength')).ToString('R', $culture))
[void]$builder.AppendLine(([double](Get-InternalProperty $point 'Heading')).ToString('R', $culture))
[void]$builder.AppendLine(([double](Get-InternalProperty $point 'UnwrappedHeading')).ToString('R', $culture))
[void]$builder.AppendLine(([int](Get-InternalProperty $point 'Source')).ToString($culture))
}
$bytes = [Text.Encoding]::UTF8.GetBytes($builder.ToString())
$hash = [Security.Cryptography.SHA256]::Create().ComputeHash($bytes)
$digest = -join ($hash | ForEach-Object { $_.ToString('x2') })
return $digest
}
Iterate LastProbeDiagnostic.CandidateLedger, not only the builder's returned canonical list, call Get-CandidateLedgerDigest $ledgerEntry $preparedSegment, and compute this digest independently from every ledger entry's own retained candidate. The stable key is single-turn/s<segmentIndex>/r<regionIndex>/w<windowIndex>/seed<seedIndex> with every index zero-based. The atomic record also stores each hard node's recovered vehicle curvature (directionSign * geometricCurvature) and fails closed if either geometric or recovered value is non-finite.
Canonical records use RecordKind=Canonical, run the evaluator directly, and may enter state counts/diagnostic selection. Deduplicated records use RecordKind=DeduplicatedAlias, retain their independent stable key/content digest in both roots, store the exact equivalence grid/proof and EvaluationSourceStableKey of the canonical record, and reuse that canonical evaluator result only as annotated evidence. Aliases are never counted in CandidateCount, StrictQualifiedCount, PhysicalCandidateCount, ExcursionQualifiedRecords, performance evaluator-call counts, or minimum-excursion selection. Missing canonical references, alias cycles, cross-root alias digest/proof mismatch, or a canonical source that was not directly evaluated makes the probe invalid.
Every child atomic record therefore contains StableKey, CandidateSha256, RecordKind, CanonicalCandidateIndex, EvaluationSourceStableKey, EquivalenceParameters, MaximumEquivalenceCoordinateDifferenceMeters, EvaluatorExecutedDirectly, evaluator result/stop-gate fields, and the interval/candidate diagnostics. For canonical records, EvaluationSourceStableKey=StableKey, EvaluatorExecutedDirectly=true, and the equivalence difference is 0. For aliases, EvaluationSourceStableKey resolves to a canonical record, EvaluatorExecutedDirectly=false, and evaluator result/stop-gate fields are copied from that source only after the reference audit succeeds.
- Step 7: Run strict GREEN twice and commit the frozen core
dotnet build ClumsyPilot/ClumsyPilot.csproj --configuration Debug
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_curve.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_curvature_probe_solver.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1 -Mode Strict
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1 -Mode Strict
git diff --check
git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2ExcursionProbeDiagnostics.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateGeometry.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2PreSmoothingPipeline.cs ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1
git commit -m 'feat: integrate bounded Local G2 excursion candidates'
$coreCommit = (git rev-parse HEAD).Trim()
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1 `
-Mode Strict -EvidenceRoot $probeEvidenceRoot -RunLabel frozen-core
$frozenCoreJsonPath = Join-Path $probeEvidenceRoot 'frozen-core.json'
$frozenCoreManifestPath = Join-Path $probeEvidenceRoot 'frozen-core-probe-source-manifest.txt'
if (!(Test-Path -LiteralPath $frozenCoreJsonPath) -or !(Test-Path -LiteralPath $frozenCoreManifestPath)) {
throw 'Frozen core evidence is incomplete.'
}
$frozenCoreJson = Get-Content -Raw -Encoding UTF8 -LiteralPath $frozenCoreJsonPath | ConvertFrom-Json
$manifestFileSha = (Get-FileHash -Algorithm SHA256 -LiteralPath $frozenCoreManifestPath).Hash.ToLowerInvariant()
if ($frozenCoreJson.Identity.CoreCommit -ne $coreCommit -or
$frozenCoreJson.Identity.RootCommit -ne $coreCommit -or
$frozenCoreJson.Identity.ProbeSourceManifestSha256 -ne $manifestFileSha) {
throw 'Frozen core identity or probe-source manifest hash mismatch.'
}
Expected: all structural assertions pass. This task does not require a particular feasibility outcome; strict candidate results are evidence for Task 4. Record the frozen core commit, evaluator hash, canonical probe-source manifest path/hash, and frozen-core JSON path. Do not edit the core after review.
Task 4: Build strict/measurement roots and execute the exhaustive feasibility gate
Files:
- Modify only in measurement clone:
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs - Use unchanged in both clones:
ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1 - Generate without committing:
.superpowers/sdd/local-g2-minimum-curvature-excursion-feasibility-report.md - Record without committing:
.superpowers/sdd/local-g2-excursion-task-4-report.md
Interfaces:
- Consumes the frozen Task 3 core commit.
- Produces two roots with a shared parent commit, one measurement-only evaluator commit, per-candidate
CandidateAuditRecordJSON, 5+30 timing data, one exhaustive state, and a generated Markdown report.
The measurement evaluator adds only private trace storage plus this disposable reflection snapshot:
public sealed class MeasurementTraceSnapshot
{
public int CandidateIndex { get; }
public double? RawMinimumCurvature { get; }
public double? RawMaximumCurvature { get; }
public double? CandidateMinimumCurvature { get; }
public double? CandidateMaximumCurvature { get; }
public double? Eminus { get; }
public double? Eplus { get; }
public bool? RawRangeExceeded { get; }
public double? VehicleMaximumCurvatureLimitPerMeter { get; }
public double? CandidateMaximumAbsoluteVehicleCurvaturePerMeter { get; }
public double? MaximumDeviationMeters { get; }
public double? MaximumDeviationLimitMeters { get; }
public bool? CollisionAndMapBoundaryPassed { get; }
public double? MinimumBodyClearanceMeters { get; }
public double? MinimumClearanceReserveMeters { get; }
public double? RawPeakCurvatureDerivativePerSquareMeter { get; }
public double? CandidatePeakCurvatureDerivativePerSquareMeter { get; }
public double? MinimumPeakGradientImprovementRatio { get; }
public double? RawCurvatureVariationCost { get; }
public double? CandidateCurvatureVariationCost { get; }
public double? MaximumVariationCostRegressionRatio { get; }
public double? AbsolutePathLengthChangeMeters { get; }
public IReadOnlyList<string> ExecutedGates { get; }
public IReadOnlyList<string> PassedGates { get; }
public string StopGate { get; }
public string Result { get; }
public string Reason { get; }
}
private static class LocalG2ExcursionMeasurementTrace
{
internal static void Begin(int candidateIndex);
internal static void RecordGate(string gate, bool passed);
internal static void RecordRawRange(int candidateIndex,
double rawMinimumCurvature, double rawMaximumCurvature,
double candidateMinimumCurvature, double candidateMaximumCurvature,
bool rawRangeExceeded);
internal static void RecordVehicleCurvature(double? vehicleLimit,
double? candidateMaximumAbsoluteVehicleCurvature, bool passed);
internal static void RecordDeviation(double? maximumDeviation,
double maximumDeviationLimit, bool passed);
internal static void RecordCollisionAndMapBoundary(bool passed,
double? minimumBodyClearance, string reason);
internal static void RecordClearance(double? minimumBodyClearance,
double minimumClearanceReserve, bool passed);
internal static void RecordPeakGradient(double rawPeak, double candidatePeak,
double minimumImprovementRatio, bool passed);
internal static void RecordVariationCost(double rawCost, double candidateCost,
double maximumRegressionRatio, bool passed);
internal static void RecordRawFullPathAnalysis(
double? absolutePathLengthChangeMeters, bool passed);
internal static void Complete(string stopGate,
PathSmoothingRegionFailureReason result, bool accepted, string reason);
internal static MeasurementTraceSnapshot Capture();
}
// Add this method to the existing public static TestHooks class.
public static MeasurementTraceSnapshot CaptureLastMeasurementTrace();
Begin replaces the previous candidate trace at the first line of Evaluate. Store the trace in a [ThreadStatic] field because the probe is sequential. Use these gate names in this order: InputAnalysis, VehicleCurvature, RawCurvatureRange, Deviation, FullPathAnalysis, CollisionAndMapBoundary, Clearance, PeakGradientImprovement, VariationCost, RawFullPathAnalysis, Accepted. Each metric-specific recorder atomically stores its operands and appends its named gate to ExecutedGates; it appends the gate to PassedGates only when passed=true. RecordRawRange similarly always appends RawCurvatureRange to executed and appends it to passed only when rawRangeExceeded=false; when exceeded, the measurement clone records a failed raw-range gate and continues. Nullable recorder operands are null only when the underlying computation was unavailable or non-finite; never serialize a non-finite double or substitute zero. On validator failure, RecordCollisionAndMapBoundary(false,null,reason) records the exact existing reason and leaves clearance null; on validator success it records the returned finite clearance, then RecordClearance records the reserve and pass/fail result. RecordPeakGradient and RecordVariationCost preserve both raw/candidate operands and configured ratios even on their rejection branches; the script derives improvement/regression percentages and leaves a ratio null when its denominator is zero. RecordRawFullPathAnalysis(null,false) is used when raw full-path analysis fails; on success it records the absolute path-length change. Use plain RecordGate only for InputAnalysis, FullPathAnalysis, and Accepted. Call the appropriate recorder immediately before every rejection and after each success, and call Complete before every return. Capture returns an immutable copy, and CaptureLastMeasurementTrace only forwards that copy. The probe script must call Evaluate for one candidate, immediately capture its trace before evaluating the next candidate, and store the copy in that candidate's atomic record. It then separately invokes the full pipeline once per run to record the probe Complete status; it must not infer per-candidate traces from the last full-pipeline evaluation. Eminus=max(0,rawMinimum-candidateMinimum) and Eplus=max(0,candidateMaximum-rawMaximum).
Complete sets Result to the exact string Accepted when accepted=true; otherwise it sets Result=result.ToString(). This mapping is invariant across processes and is part of the determinism signature.
Nullable range/excursion fields remain null until RecordRawRange; JSON preserves null, and the Markdown renderer writes it as not evaluated, never 0.
- Step 1: Clone the frozen core twice and prove source identity
$strictRoot = Join-Path $probeRoot 'strict-root'
$measureRoot = Join-Path $probeRoot 'measurement-root'
$evidenceRoot = Join-Path $probeRoot 'evidence'
New-Item -ItemType Directory -Path $evidenceRoot -Force | Out-Null
git -c core.autocrlf=false clone $coreRoot $strictRoot
git -c core.autocrlf=false clone $coreRoot $measureRoot
git -C $strictRoot config core.autocrlf false
git -C $measureRoot config core.autocrlf false
$coreCommit = (git -C $coreRoot rev-parse HEAD).Trim()
if ((git -C $strictRoot rev-parse HEAD).Trim() -ne $coreCommit) { throw 'Strict clone drift.' }
if ((git -C $measureRoot rev-parse HEAD).Trim() -ne $coreCommit) { throw 'Measurement clone drift.' }
if (Test-Path (Join-Path $strictRoot 'ClumsyPilot\ParkrobTrajplanner\auto_avoidance')) { throw 'Strict root contains excluded source.' }
if (Test-Path (Join-Path $measureRoot 'ClumsyPilot\ParkrobTrajplanner\auto_avoidance')) { throw 'Measurement root contains excluded source.' }
if ((Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $strictRoot '.gitattributes')) -ne "* -text`n") { throw 'Strict root EOL policy drift.' }
if ((Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $measureRoot '.gitattributes')) -ne "* -text`n") { throw 'Measurement root EOL policy drift.' }
if ((git -C $strictRoot config --get core.autocrlf).Trim() -ne 'false') { throw 'Strict root autocrlf drift.' }
if ((git -C $measureRoot config --get core.autocrlf).Trim() -ne 'false') { throw 'Measurement root autocrlf drift.' }
Hash shared snapshot, core, strict, and measurement evaluators before the measurement patch. All four hashes must match.
Load $probeEvidenceRoot/frozen-core.json and $probeEvidenceRoot/frozen-core-probe-source-manifest.txt from Task 3. Require the JSON core/root commit to equal $coreCommit, require the canonical file's raw-byte SHA-256 to equal Identity.ProbeSourceManifestSha256, and retain that value as $expectedProbeSourceManifestSha256. Later strict and measurement children must each recompute exactly this value from their own clean filesystem root; because the evaluator is excluded, the measurement evaluator commit does not change it.
- Step 2: Write the measurement RED assertion before changing the evaluator
Run the unchanged probe in measurement mode:
dotnet build ClumsyPilot/ClumsyPilot.csproj --configuration Debug
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1 `
-Mode Measurement -CoreCommit $coreCommit
Expected: FAIL with exactly Measurement evaluator hook is unavailable. It must not silently treat the strict early return as measurement success.
- Step 3: Add the only permitted measurement behavior
In measurement-root evaluator, preserve the existing vehicle gate, compute rawRangeExceeded, record it, and reject only when not in the measurement-only clone. Because this source exists only in that clone, bind it as:
private const bool MeasureRawCurvatureExcursion = true;
Replace only the existing raw-range return with:
bool rawRangeExceeded = ExceedsRawCurvatureRange(
candidateAnalysis.Path, rawMinimumCurvature, rawMaximumCurvature);
GetCurvatureRange(candidateAnalysis.Path,
out double candidateMinimumCurvature, out double candidateMaximumCurvature);
LocalG2ExcursionMeasurementTrace.RecordRawRange(
candidateIndex, rawMinimumCurvature, rawMaximumCurvature,
candidateMinimumCurvature, candidateMaximumCurvature, rawRangeExceeded);
if (rawRangeExceeded && !MeasureRawCurvatureExcursion)
return Rejected(candidateIndex, PathSmoothingRegionFailureReason.CurvatureOvershoot,
"局部 G2 候选超出原始区域曲率范围。");
Add the exact trace calls above immediately after each metric becomes available, after each later gate is actually passed, and at each later failure return. In particular, do not read quality metrics from Rejected(...), because that helper replaces unavailable values with zeros: copy the live evaluator locals into the metric recorders before returning. Trace state is reset for every candidate and is only exposed through the existing nested reflection TestHooks; it must not change return values, ordering, analysis inputs, validator calls, or selection.
- Step 4: Run measurement tests and commit only the evaluator in the measurement clone
dotnet build ClumsyPilot/ClumsyPilot.csproj --configuration Debug
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_excursion_probe.ps1 `
-Mode Measurement -CoreCommit $coreCommit
git diff --check
git add -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs
git commit -m 'test: measure Local G2 raw curvature excursion'
Expected: the script reaches every later gate for candidates that pass earlier gates and emits execution flags. The commit must contain exactly one file.
The measurement-mode script must also exercise the existing deterministic evaluator scenarios by calling LocalG2CandidateEvaluator.TestHooks.Execute(scenario) and immediately calling CaptureLastMeasurementTrace() after each call. Assert: TooFar records finite deviation and its 0.10 m limit before rejecting Deviation; LowClearance records finite checked clearance and the 0.02 m reserve before rejecting Clearance; NoOp records raw/candidate peak-gradient operands plus the 0.20 ratio before rejecting PeakGradientImprovement; Oscillating records raw/candidate variation costs plus the 0.02 ratio before rejecting VariationCost; Improved records every metric field, all named gates, and a finite absolute path-length change before Accepted; and Overshoot records RawCurvatureRange as executed-but-not-passed while proving at least the next Deviation gate executed. For each early rejection, all later metric fields remain null and render not evaluated, not zero.
- Step 5: Produce atomic strict/measurement candidate records
Run strict and measurement roots sequentially and compare by stable key. For every key require identical candidate SHA-256; pair strict result, measurement result, stop gate, execution flags, raw/candidate ranges, Eminus/Eplus, improvement, variation, deviation, clearance, and elapsed metrics in one record.
The strict side qualifies an excursion pair only when its result is exactly CurvatureOvershoot. CurvatureExceeded, CandidateGenerationFailed, any earlier result, digest mismatch, or missing gate flag cannot support FEASIBLE_WITH_EXCURSION.
Derive StrictStopGate without modifying the strict evaluator: Accepted -> Accepted, CurvatureExceeded -> VehicleCurvature, CurvatureOvershoot -> RawCurvatureRange, DeviationExceeded -> Deviation, Collision -> CollisionAndMapBoundary, InsufficientClearance -> Clearance, InsufficientImprovement -> PeakGradientImprovement, and VariationCostRegression -> VariationCost. For CandidateGenerationFailed, use the paired measurement trace's InputAnalysis, VehicleCurvature, FullPathAnalysis, or RawFullPathAnalysis stop gate only when the trace either stopped before raw-range evaluation or recorded RawRangeExceeded=false; if it recorded an exceeded raw range, the unchanged strict result should have been CurvatureOvershoot, so the pair is invalid. No reason-string guessing is permitted.
Every single-run Strict or non-batch Measurement JSON uses schema version 1 and contains these exact top-level fields:
SchemaVersion
Mode
RunLabel
Environment { MachineName, CpuName, LogicalProcessorCount, TotalMemoryBytes, OsDescription,
PowerShellVersion, DotNetSdkVersion, BuildConfiguration, TargetFramework }
Identity { SharedBaselineCommit, CoreCommit, RootCommit, FixtureSha256,
EvaluatorSha256, ProbeSourceManifestSha256 }
ProbeRunDiagnostic
CandidateRecords
PipelineStatus
PipelineSelectedStableKey
Timings { IntervalSolverElapsedMilliseconds[], SolverPhaseElapsedMilliseconds,
CandidateEvaluationElapsedMilliseconds[], CandidateEvaluationPhaseElapsedMilliseconds,
RegionElapsedMilliseconds, FixtureElapsedMilliseconds }
A Measurement -Batch JSON has exact top-level fields SchemaVersion, Mode, RunLabel, Environment, Identity, WarmupRuns, and MeasuredRuns. WarmupRuns has exactly five single-run payloads and MeasuredRuns exactly 30, both in execution order; nested payloads omit duplicate Environment/Identity but otherwise use the same ProbeRunDiagnostic, CandidateRecords, PipelineStatus, PipelineSelectedStableKey, and Timings fields.
Before timing, compare the batch environment with the recorded baseline in the shared scratch file .superpowers/sdd/local-g2-split-scale-feasibility-report.md: machine DESKTOP-428CCNK, CPU 11th Gen Intel Core i5-1135G7 @ 2.40 GHz, 8 logical processors, 16,863,318,016 RAM bytes, Windows 10 Pro 10.0.19045, PowerShell 5.1.19041.7548, .NET SDK 10.0.302, and Debug/netstandard2.0. A mismatch sets InvalidEvidence=true; record both observed and expected metadata rather than silently changing the performance baseline.
IntervalSolverElapsedMilliseconds records every successful or failed invocation in order. SolverPhaseElapsedMilliseconds covers all closure calls in the region. Candidate evaluation timings record every evaluator call and their sum. RegionElapsedMilliseconds covers candidate construction, all candidate evaluations, selection, and region result. FixtureElapsedMilliseconds covers the full SingleTurn probe run after assembly/fixture setup. All timing fields use Stopwatch.GetTimestamp, are finite and nonnegative, and exclude process launch/build. FinalGate additionally reports strict-child, measurement-child, pairing, and two-root total diagnostic wall time separately; these diagnostic totals never enter the four performance gates.
For every paired atomic record compute:
denominator = max(rawMaximum - rawMinimum,
AbsoluteCurvatureJumpFloorPerMeter)
Rminus = Eminus / denominator
Rplus = Eplus / denominator
analyticVehicleCurvatureUtilization = max(abs(c0),abs(c1),abs(c2),abs(c3)) / Kvehicle
discreteVehicleCurvatureUtilization = candidateMaximumAbsoluteVehicleCurvature / Kvehicle
The record contains HardPhysicalQualified, which is true only when every interval passes hard closure/seam/nondegeneracy checks, all four continuous controls pass the physical curvature range, and the unchanged evaluator actually passes its discrete vehicle-curvature gate. Missing or non-finite operands make the record invalid, never zero.
New-PairedObservation -StrictAudit <json> -MeasurementRun <json> performs this deterministic join: validate schema/identity/source hashes, including requiring both ProbeSourceManifestSha256 values to equal $expectedProbeSourceManifestSha256 and requiring each written canonical manifest file to be raw-byte identical to the frozen-core manifest; require the same ordered stable-key set; for each key require equal candidate digest, RecordKind, canonical index, evaluation-source key, fixed equivalence grid, and equivalence proof before copying both child records into one CandidateAuditRecord; compute excursion/utilization fields for canonical records only; verify every required executed-gate flag; derive HardPhysicalQualified; retain both roots' pipeline statuses and selected keys; sort atomic records by stable key; then populate InvalidEvidence, StrictQualifiedCount, PhysicalCandidateCount, and ExcursionQualifiedRecords from canonical records only. It returns a new object and never mutates either child JSON. It also proves each alias points to one directly evaluated canonical record, rejects self-reference/cycles, and proves aliases never enter evaluator-call/performance/eligibility/selection counts. A missing/duplicate key, digest or alias-proof mismatch, probe-manifest mismatch, non-finite required number, missing flag, or Complete pipeline selection that names no canonical atomic record sets InvalidEvidence=true and records the exact reason list. Strict and measurement selected keys may differ because measurement can admit additional raw-range-excursion candidates; both keys remain determinism evidence but do not filter otherwise accepted per-candidate canonical records.
- Step 6: Execute the fixed 5+30 performance and determinism protocol
function Invoke-ProbeChild {
param(
[Parameter(Mandatory=$true)][string]$Root,
[Parameter(Mandatory=$true)][ValidateSet('Strict','Measurement')][string]$ChildMode,
[Parameter(Mandatory=$true)][string]$ChildRunLabel,
[Parameter(Mandatory=$true)][string]$CoreCommit,
[switch]$ChildBatch
)
$scriptPath = Join-Path $Root 'ClumsyPilot\tests\verify_path_smoothing_local_g2_excursion_probe.ps1'
$jsonPath = Join-Path $EvidenceRoot ($ChildRunLabel + '.json')
$childArgs = @('-ExecutionPolicy','Bypass','-File',$scriptPath,'-Mode',$ChildMode,
'-EvidenceRoot',$EvidenceRoot,'-RunLabel',$ChildRunLabel,'-CoreCommit',$CoreCommit)
if ($ChildBatch) { $childArgs += '-Batch' }
& powershell @childArgs
if ($LASTEXITCODE -ne 0) { throw "$ChildMode child failed for $ChildRunLabel." }
if (!(Test-Path -LiteralPath $jsonPath)) { throw "Missing child JSON: $jsonPath" }
return Get-Content -Raw -Encoding UTF8 -LiteralPath $jsonPath | ConvertFrom-Json
}
$strictAudit = Invoke-ProbeChild -Root $StrictRoot -ChildMode Strict `
-ChildRunLabel 'strict-audit' -CoreCommit $coreCommit
$measurementBatch = Invoke-ProbeChild -Root $MeasurementRoot -ChildMode Measurement `
-ChildRunLabel 'measurement-batch' -CoreCommit $coreCommit -ChildBatch
if (@($measurementBatch.WarmupRuns).Count -ne 5) { throw 'Measurement batch must contain 5 warm-ups.' }
if (@($measurementBatch.MeasuredRuns).Count -ne 30) { throw 'Measurement batch must contain 30 measured runs.' }
$warmups = @($measurementBatch.WarmupRuns | ForEach-Object {
New-PairedObservation -StrictAudit $strictAudit -MeasurementRun $_
})
$runs = @($measurementBatch.MeasuredRuns | ForEach-Object {
New-PairedObservation -StrictAudit $strictAudit -MeasurementRun $_
})
$measurementAudit = $runs[0]
The measurement batch child loads its assembly and fixture exactly once, then runs 5+30 serial iterations with the same immutable SingleTurn request and CancellationToken.None in that process; each iteration constructs fresh builder/evaluator/pipeline instances so only JIT/runtime warming persists. The strict child is a separate process so same-name assemblies never share a loader. New-PairedObservation creates atomic records, marks InvalidEvidence on identity/key/digest mismatch, and determines HardPhysicalQualified. When evidence is valid it runs the Step 7 underlying-state function with the fixed strict audit plus that measurement run; when invalid it sets paired state to PROBE_INVALID and leaves selection not evaluated. It returns PairedState, UnderlyingStateWithoutPerformanceGate, and SelectedStableKey beside the measurement timings. Thus a Measurement child never invents a final state by itself.
Build each measured determinism signature by sorting paired atomic records by stable key and hashing invariant JSON containing only key, candidate SHA-256, RecordKind, canonical index, evaluation-source key, equivalence grid/proof, direct-evaluation flag, strict/measurement results, stop gates, execution flags, interval diagnostics, PairedState, UnderlyingStateWithoutPerformanceGate, and SelectedStableKey—never elapsed values or file paths. Assert all 30 signatures match and retain all five warm-up records plus all 30 measured records in evidence. Sort the 30 SolverPhaseElapsedMilliseconds values and the 30 RegionElapsedMilliseconds values separately; for each, use the mean of elements 14 and 15 (zero-based) as the median and Measure-Object -Maximum as worst. Apply all four gates: solver median/worst <=10/25 ms, region median/worst <=25/50 ms. Also report every per-invocation, evaluator-phase, fixture, and two-root diagnostic timing without applying additional pass/fail thresholds.
- Step 7: Apply the frozen, exhaustively tested ordered state machine
Create one observation object after pairing. InvalidEvidence is true for any design section 11.1 condition: unknown commits/root/fixture, source-diff violation, strict evaluator mismatch, key/digest mismatch, missing or non-finite required data, missing gate flags/diffs/repeat evidence, infrastructure failure, cancellation, or unequal 30-run signatures. PerformanceExceeded is true when any of the four fixed median/worst limits fails. StrictQualifiedCount counts atomic records whose hard/physical gates pass, whose strict evaluator and measurement evaluator both accept all unchanged gates, and whose strict and measurement full pipelines both report probe Complete; per-candidate direct acceptance is the qualification, while each pipeline's selected key is retained only as audit/determinism evidence. PhysicalCandidateCount counts HardPhysicalQualified records. ExcursionQualifiedRecords contains all hard/physical records with strict result exactly CurvatureOvershoot, measurement result Accepted, every later gate executed and passed, measurement pipeline status Complete, valid source identity, and Eminus>0 or Eplus>0; do not discard an accepted candidate merely because the pipeline selected another accepted candidate.
The unchanged Task 3 script already contains and table-tests these exact functions; execute them against the paired evidence:
function Resolve-UnderlyingStateWithoutPerformanceGate {
param($Observation)
if ($Observation.StrictQualifiedCount -gt 0) { return 'STRICT_GREEN' }
if ($Observation.PhysicalCandidateCount -eq 0) {
return 'NO_PHYSICALLY_ADMISSIBLE_CLOSED_CANDIDATE_FOUND_WITHIN_BUDGET'
}
if (@($Observation.ExcursionQualifiedRecords).Count -eq 0) { return 'QUALITY_RED' }
return 'FEASIBLE_WITH_EXCURSION'
}
function Resolve-FinalState {
param($Observation)
if ($Observation.InvalidEvidence) { return 'PROBE_INVALID' }
$underlying = Resolve-UnderlyingStateWithoutPerformanceGate $Observation
if ($Observation.PerformanceExceeded) { return 'BUDGET_RED' }
return $underlying
}
Before using real observations, table-test: each of the six states alone; InvalidEvidence+PerformanceExceeded -> PROBE_INVALID; performance failure plus a strict candidate -> BUDGET_RED; strict-qualified plus excursion-qualified -> STRICT_GREEN; physical zero -> NO_PHYSICALLY_ADMISSIBLE_CLOSED_CANDIDATE_FOUND_WITHIN_BUDGET; physical positive plus zero excursion -> QUALITY_RED; digest mismatch -> PROBE_INVALID. Enumerate every logically legal combination of the four booleans/count predicates, assert the return belongs to the six-state set, and assert exactly one string is returned. Any failure exits nonzero as a broken test contract.
Always record UnderlyingStateWithoutPerformanceGate; it is especially mandatory when final state is BUDGET_RED. For FEASIBLE_WITH_EXCURSION, select only ExcursionQualifiedRecords by (max(Eminus,Eplus), Eminus+Eplus, maximumDeviation, candidateIndex). For STRICT_GREEN, report that no raw-range relaxation is needed. For the budget/no-candidate/quality states, retain every categorized failure count. Do not turn not evaluated into zero.
The generated report must contain every field required by spec sections 12–15 plus full paths to:
strict-vs-shared evaluator hash proof
measurement evaluator commit and diff
strict/measurement candidate audit JSON
30-run timing JSON
state decision trace
- Step 8: Independently verify root diffs and rerun the final gate
git -C $strictRoot diff --quiet $coreCommit HEAD
if ($LASTEXITCODE -ne 0) { throw 'Strict root has uncommitted drift.' }
$strictStatus = @(git -C $strictRoot status --porcelain)
if ($strictStatus.Count -ne 0) { throw ('Strict root working tree drift: ' + ($strictStatus -join ',')) }
$measureFiles = @(git -C $measureRoot diff --name-only $coreCommit HEAD)
if ($measureFiles.Count -ne 1 -or $measureFiles[0] -ne 'ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs') {
throw ('Unexpected measurement changes: ' + ($measureFiles -join ','))
}
$measureStatus = @(git -C $measureRoot status --porcelain)
if ($measureStatus.Count -ne 0) { throw ('Measurement root working tree drift: ' + ($measureStatus -join ',')) }
$finalGateScript = Join-Path $measureRoot 'ClumsyPilot\tests\verify_path_smoothing_local_g2_excursion_probe.ps1'
powershell -ExecutionPolicy Bypass -File $finalGateScript -Mode FinalGate `
-StrictRoot $strictRoot -MeasurementRoot $measureRoot -EvidenceRoot $evidenceRoot
The controller reruns the same final-gate command independently after the task reviewer approves. Task 4 is complete when evidence is valid and exactly one state is produced; it does not require the state to be positive.
Task 5: Publish only the verified feasibility report on the shared branch
Files:
- Create on shared branch:
docs/superpowers/reports/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-report.md - Record without committing:
.superpowers/sdd/local-g2-excursion-task-5-report.md
Interfaces:
-
Consumes the Task 4 generated report and controller rerun evidence.
-
Produces one immutable shared report; no probe
.cs,.ps1, configuration, fixture, or evaluator file is copied back. -
Step 1: Verify the shared branch has no staged files and no probe source drift
git diff --cached --quiet
if ($LASTEXITCODE -ne 0) { throw 'Shared index is not empty.' }
git diff --no-ext-diff a4e116a -- ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateBuilder.cs ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/LocalG2CandidateEvaluator.cs
The diff is recorded as context and is not compared with a4e116a, because the shared tree already contained user changes at Task 1. The authoritative preservation check is the Task 1 machine-readable manifest: regenerate it from the same explicit ClumsyPilot.csproj, ParkrobTrajplanner/PathSmoothing/**/*.cs, and fixture paths, then require identical path count, sorted paths, per-file SHA-256 values, and manifest SHA-256. This includes the currently untracked LocalG2PreSmoothingPipeline.cs.
- Step 2: Add the generated report with
apply_patch
Use the exact Task 4 generated Markdown as the new report. It must name the final state, core/strict/measurement commits and paths, all source hashes/diffs, candidate atomic records, timing samples, decision trace, and the matching authorization boundary:
PROBE_INVALID -> repair and rerun; no geometry conclusion
BUDGET_RED -> performance-only optimization and rerun
STRICT_GREEN -> next design keeps the strict raw-range gate
NO_PHYSICALLY_ADMISSIBLE_CLOSED_CANDIDATE_FOUND_WITHIN_BUDGET or QUALITY_RED -> no evaluator change
FEASIBLE_WITH_EXCURSION -> report minimum found excursion; request a new production-envelope decision
- Step 3: Verify and commit only the report
git add -- docs/superpowers/reports/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-report.md
git diff --cached --check
$staged = @(git diff --cached --name-only)
if ($staged.Count -ne 1 -or $staged[0] -ne 'docs/superpowers/reports/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-report.md') {
throw ('Unexpected staged files: ' + ($staged -join ','))
}
git commit -m 'docs: report Local G2 curvature excursion feasibility'
- Step 4: Controller final verification
Freshly run:
git show --check --stat HEAD
git diff --cached --quiet
git status --short -- docs/superpowers/reports/2026-08-01-local-g2-minimum-curvature-excursion-feasibility-report.md
Then compare the committed report SHA-256 with the Task 4 generated report SHA-256. Only after equality and the independent final-gate rerun may the controller state the probe result to the user.
Stop Rules
- Stop immediately with
PROBE_INVALIDon source identity drift, unauthorized evaluator differences, digest mismatch, missing evidence, nondeterminism, cancellation, or test infrastructure failure. BUDGET_RED,NO_PHYSICALLY_ADMISSIBLE_CLOSED_CANDIDATE_FOUND_WITHIN_BUDGET, andQUALITY_REDare valid terminal evidence outcomes, not permission to expand search or relax gates.STRICT_GREENauthorizes only a new production design that keeps the strict raw-range evaluator unchanged.FEASIBLE_WITH_EXCURSIONauthorizes only a user decision and a new production-envelope design; it does not authorize copying measurement behavior or probe source into production.- Do not execute any old split-scale plan task after this plan begins.
Subagent-Driven Review Gates
- Record the disposable core baseline commit before Task 1 implementation and use it as the review-package base for Task 1.
- For Tasks 2 and 3, use the previous approved disposable-core commit as the review-package base; never use
HEAD~1when a task creates multiple commits. - Task 4 review receives the core commit, measurement commit, strict/measurement evaluator diff, candidate-audit JSON, timing JSON, state trace, and generated report.
- Resolve every task reviewer Critical/Important before continuing. A
⚠️ Cannot verifyitem must be resolved by the controller before marking the task complete. - After Task 5, perform a broad final review of the design commit, plan commit, final report, and all disposable evidence paths. The final reviewer must confirm that no probe source reached the shared branch.