diff --git a/docs/superpowers/plans/2026-08-06-em-full-direction-segment-visualization-repair.md b/docs/superpowers/plans/2026-08-06-em-full-direction-segment-visualization-repair.md new file mode 100644 index 0000000..f1ad436 --- /dev/null +++ b/docs/superpowers/plans/2026-08-06-em-full-direction-segment-visualization-repair.md @@ -0,0 +1,1107 @@ +# EM Full-Direction-Segment Planning and Visualization Repair 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:** Add one-shot full-direction-segment LS/ST planning that starts from rest, reaches the configured cruise speed when feasible, stops at the true segment boundary with pose tolerances, and repairs the existing paper-style observation UI without adding actuator output. + +**Architecture:** Keep rolling planning intact behind an explicit `RollingHorizon` scope and add `FullDirectionSegment` as a separate request scope. Full-segment planning selects the remaining active direction segment, derives a bounded adaptive optimization schedule from the actual Local G2 `s_end` and jerk-limited speed envelope, then publishes a separately resampled trajectory. Observation snapshots remain immutable; Web and Native Painter consume the same corrected semantics, while a test-only jsdom host verifies actual DOM/SVG output. + +**Tech Stack:** C#/.NET 8, existing EM planner and OSQP abstraction, existing MovementTest observer, embedded HTML/CSS/vanilla JavaScript/SVG/Canvas, Node.js 24 test runtime with `jsdom` 29.1.1 as a development-only DOM dependency. + +## Global Constraints + +- MovementTest remains `OBSERVE_ONLY`; do not add chassis, steering, brake, motor, or gear write calls. +- “Global” means the complete remaining part of one coarse-path direction segment. A trajectory never crosses a forward/reverse boundary. +- `FullDirectionSegment` plans once when a segment becomes active and reuses the frozen result. Runtime rolling refinement and dynamic-obstacle replanning are outside this plan. +- If MovementTest starts in the middle of a segment, plan from the valid Local G2 projection of the startup pose to that segment’s true end boundary. +- Full mode derives `s_end` from actual Local G2 `PathS` and derives `T_end`; `DistanceHorizonMeters` and `TimeHorizonSeconds` do not truncate full mode. +- Forward desired/hard maximum speed is `1.0 m/s`; reverse desired/hard maximum speed is `0.5 m/s`. +- Terminal world-position error must be at most `0.03 m`; normalized yaw error must be at most `5°`. +- Published trajectory sampling is `0.1 s` for MovementTest and is independent of adaptive optimization knots. +- Jerk contains exactly `N-1` interval values for `N` trajectory points; there is no jerk sample after the terminal point. +- Resource exhaustion returns `FullSegmentResourceLimitExceeded`; it never silently truncates or falls back to rolling. +- Preserve the existing four-tab white paper/scientific Web layout, Chinese titles, mathematical symbols, and thin lines. +- Web zoom changes only the browser viewport. It never mutates snapshot data or sends planner settings. +- Native Painter receives coordinate, scale, marker, and semantic corrections only; do not redesign or beautify it. +- Before editing any already-dirty file, record `git diff -- ` and preserve all unrelated user hunks. Stage every task with explicit paths only. +- The approved design is `docs/superpowers/specs/2026-08-06-em-full-direction-segment-visualization-repair-design.md`. + +--- + +## File and Responsibility Map + +**New planner files** + +- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningScope.cs`: explicit rolling versus full-direction-segment request scope. +- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalKnotSchedule.cs`: immutable optimization-knot schedule, separate from publication sampling. +- `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/FullDirectionSegmentScheduleBuilder.cs`: derives `T_end`, adaptive knots, and reference progress/speed from Local G2 `PathS` and the speed/stopping envelope. + +**New Web-test files** + +- `ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package.json`: test-only Node package with exact jsdom dependency. +- `ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package-lock.json`: locked dependency graph generated by npm. +- `ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs`: actual DOM/SVG regression tests. + +**Existing files with focused changes** + +- Configuration/contracts: `EmPlannerConfiguration.cs`, `SchedulingConfiguration.cs`, `LongitudinalConfiguration.cs`, `ValidationConfiguration.cs`, `EmPlanningRequest.cs`, `EmPlanningStatus.cs`, `EmTrajectoryMetadata.cs`. +- Scope and full planning: `PlanningHorizonSelector.cs`, `EmPlanningRequestValidator.cs`, `EmPlanningService.cs`. +- Longitudinal model: `PathSpeedLimitBuilder.cs`, `LongitudinalPlanningInput.cs`, `SequentialLongitudinalOptimizer.cs`, `LongitudinalObjectiveBuilder.cs`, `LongitudinalConstraintBuilder.cs`, `LongitudinalSolutionValidator.cs`, `LongitudinalPreviousTrajectorySeedBuilder.cs`. +- Publication: `TrajectorySampleSchedule.cs`, `EmTrajectoryAssembler.cs`, `EmTrajectoryValidator.cs`. +- MovementTest: `TrajectoryObservationContracts.cs`, `TrajectoryObservationPipeline.cs`, `MovementTest.TrajectoryObservationTest.cs`. +- Observation semantics: `TrajectoryObservationStaticSnapshotBuilder.cs`, `TrajectoryObservationDynamicSnapshotBuilder.cs`, `TrajectoryObservationKinematicChartBuilder.cs`, `TrajectoryObservationPresentation.cs`. +- Visualization contracts/assets: `VisualizationCharts.cs`, `VisualizationGeometry.cs`, `PlanningVisualizationSnapshots.cs`, `index.html`, `app.css`, `app.js`, `EmbeddedWebAssets.cs`. +- Tests: existing EM and visualization verification hosts plus the new test-only jsdom package. +- Operator docs: MovementTest and visualization READMEs, followed by a new acceptance handoff. + +--- + +### Task 1: Add explicit planning scope and validated configuration + +**Files:** + +- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningScope.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningRequest.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningStatus.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectoryMetadata.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/SchedulingConfiguration.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LongitudinalConfiguration.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/ValidationConfiguration.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/EmPlannerConfiguration.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmPlanningRequestValidator.cs` +- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/FoundationChecks.cs` +- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` + +**Interfaces:** + +- Produces: `EmPlanningScope.RollingHorizon` and `EmPlanningScope.FullDirectionSegment`. +- Produces: `EmPlanningRequest.PlanningScope` and `EmTrajectoryMetadata.PlanningScope`. +- Produces configuration properties used by Tasks 2-5: + `DesiredForwardSpeedMetersPerSecond`, `DesiredReverseSpeedMetersPerSecond`, + `MaximumOptimizationTimeStepSeconds`, `MaximumOptimizationSpatialStepMeters`, + `MaximumOptimizationKnotCount`, `MaximumPublishedSampleCount`, + `TerminalPositionToleranceMeters`, and `TerminalYawToleranceRadians`. +- Produces status values `NoProgress`, `TerminalPoseMismatch`, and `FullSegmentResourceLimitExceeded`. + +- [ ] **Step 1: Write failing contract/default/copy tests** + +Add checks that construct both request scopes, copy the configuration, and assert the exact defaults: + +```csharp +Verification.Equal(EmPlanningScope.FullDirectionSegment, fullRequest.PlanningScope, + "full request freezes its scope"); +Verification.NearlyEqual(1.0d, defaults.Longitudinal.DesiredForwardSpeedMetersPerSecond, + "forward desired speed"); +Verification.NearlyEqual(0.5d, defaults.Longitudinal.DesiredReverseSpeedMetersPerSecond, + "reverse desired speed"); +Verification.NearlyEqual(1.0d, defaults.Longitudinal.MaximumForwardSpeedMetersPerSecond, + "forward hard limit"); +Verification.NearlyEqual(0.5d, defaults.Longitudinal.MaximumReverseSpeedMetersPerSecond, + "reverse hard limit"); +Verification.NearlyEqual(0.20d, defaults.Scheduling.MaximumOptimizationTimeStepSeconds, + "adaptive maximum dt"); +Verification.NearlyEqual(0.10d, defaults.Scheduling.MaximumOptimizationSpatialStepMeters, + "adaptive maximum ds"); +Verification.Equal(401, defaults.Scheduling.MaximumOptimizationKnotCount, + "optimization knot cap"); +Verification.Equal(5001, defaults.Scheduling.MaximumPublishedSampleCount, + "publication sample cap"); +Verification.NearlyEqual(0.03d, defaults.Validation.TerminalPositionToleranceMeters, + "terminal position tolerance"); +Verification.NearlyEqual(5d * Math.PI / 180d, defaults.Validation.TerminalYawToleranceRadians, + "terminal yaw tolerance"); +``` + +Mutate the source after `Copy()` and verify the copy remains unchanged. Add invalid-value tests for zero/negative knot caps, desired speed above the hard limit, and terminal yaw tolerance above π. + +- [ ] **Step 2: Run RED** + +```powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- foundation +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service +``` + +Expected: compilation fails because the scope, statuses, and configuration members do not exist. + +- [ ] **Step 3: Add the contracts and defaults** + +Create: + +```csharp +namespace MultiWheelC.TrajectoryPlanning.EMPlanner; + +public enum EmPlanningScope +{ + RollingHorizon, + FullDirectionSegment, +} +``` + +Add a required `EmPlanningScope planningScope` constructor parameter and immutable property to `EmPlanningRequest`. Add the same field to `EmTrajectoryMetadata`. Update every constructor call explicitly; do not add an implicit default that could silently change legacy rolling behavior. + +Add the configuration members listed above and copy them in their owning `Copy()` methods. Set the exact defaults from the test. Add the three new failure statuses to `EmPlanningStatus`. + +- [ ] **Step 4: Validate cross-field invariants** + +Extend `TryValidateConfiguration` with exact checks: + +```csharp +if (!Positive(longitudinal.DesiredForwardSpeedMetersPerSecond) || + longitudinal.DesiredForwardSpeedMetersPerSecond > longitudinal.MaximumForwardSpeedMetersPerSecond || + !Positive(longitudinal.DesiredReverseSpeedMetersPerSecond) || + longitudinal.DesiredReverseSpeedMetersPerSecond > longitudinal.MaximumReverseSpeedMetersPerSecond || + !Positive(scheduling.MaximumOptimizationTimeStepSeconds) || + !Positive(scheduling.MaximumOptimizationSpatialStepMeters) || + scheduling.MaximumOptimizationKnotCount < 3 || + scheduling.MaximumPublishedSampleCount < 2 || + !Positive(validation.TerminalPositionToleranceMeters) || + !Positive(validation.TerminalYawToleranceRadians) || + validation.TerminalYawToleranceRadians > Math.PI) +{ + return false; +} +``` + +Validate `request.PlanningScope` with `Enum.IsDefined`. Keep the rolling distance/time sufficiency validation only for `RollingHorizon`; full mode still validates positive legacy values for compatibility but does not use them as truncation inputs. + +- [ ] **Step 5: Run GREEN** + +Run the two commands from Step 2. Expected: `PASS foundation` and `PASS em-planning-service`. + +- [ ] **Step 6: Commit the contract boundary** + +```powershell +git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningScope.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningRequest.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmPlanningStatus.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Contracts/EmTrajectoryMetadata.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/SchedulingConfiguration.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/LongitudinalConfiguration.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/ValidationConfiguration.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Configuration/EmPlannerConfiguration.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmPlanningRequestValidator.cs ClumsyPilot/tests/EMPlannerVerificationHost/FoundationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs +git commit -m "feat: define full-direction EM planning scope" +``` + +--- + +### Task 2: Select the complete remaining direction segment + +**Files:** + +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/PlanningHorizonSelector.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs` +- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` +- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` + +**Interfaces:** + +- Consumes: `EmPlanningScope` from Task 1. +- Produces: `PlanningHorizonSelector.Select(..., EmPlanningScope planningScope, ...)`. +- Guarantees: full mode returns the real `Goal` or `GearSwitch` boundary with `ExactStopAtBoundary`; rolling mode preserves existing rolling/approach/exact selection. + +- [ ] **Step 1: Write failing scope-selection tests** + +Create a 10 m segment, project the vehicle at segment-local 3 m, set legacy distance horizon to 1 m and time horizon to 2 s, then assert: + +```csharp +EmPlanningStatus status = selector.Select(segment, 3d, 0d, 0d, + EmPlanningScope.FullDirectionSegment, configuration, out PlanningHorizonSelection full, out string failure); +Verification.Equal(EmPlanningStatus.Success, status, "full selection: " + failure); +Verification.NearlyEqual(segment.LengthMeters, full.WindowEndReferenceS, + "full selection reaches the true segment boundary"); +Verification.Equal(EmLongitudinalMode.ExactStopAtBoundary, full.LongitudinalMode, + "full selection always plans a stopped boundary"); +Verification.True(full.HasStopBoundary, "full selection retains a real stop boundary"); +``` + +Use the same fixture with `RollingHorizon` and assert the window ends at 4 m. Add a gear-pair fixture and assert segment 0 ends at `GearSwitch` without including segment 1 points. + +- [ ] **Step 2: Run RED** + +```powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model +``` + +Expected: compile failure because `Select` does not accept planning scope. + +- [ ] **Step 3: Implement the explicit full branch** + +Change the selector signature and branch after the initial-state/stopping-distance precheck: + +```csharp +if (planningScope == EmPlanningScope.FullDirectionSegment) +{ + EmBoundaryType boundary = segment.EndBoundary.BoundaryType; + if (!IsStopBoundary(boundary)) + { + failureReason = "A full direction segment must end at Goal or GearSwitch."; + return EmPlanningStatus.InvalidReferencePath; + } + selection = new PlanningHorizonSelection(segment.LengthMeters, boundary, + ToTerminalType(boundary), EmLongitudinalMode.ExactStopAtBoundary, + segment.LengthMeters, true); + return EmPlanningStatus.Success; +} +``` + +Leave the existing rolling selection below this branch unchanged. Pass `request.PlanningScope` from `EmPlanningService` and include the scope in metadata and diagnostics. + +- [ ] **Step 4: Verify the service slices from the startup projection to the real boundary** + +Add an end-to-end service test where the initial pose projects into the middle of a direction segment. Assert the first published point is at the projected startup pose, the final point is the segment endpoint, and no point belongs to the next direction segment. + +- [ ] **Step 5: Run GREEN** + +```powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service +``` + +Expected: both commands print PASS. + +- [ ] **Step 6: Commit segment selection** + +```powershell +git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Segmentation/PlanningHorizonSelector.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs +git commit -m "feat: select complete EM direction segments" +``` + +--- + +### Task 3: Derive adaptive full-segment optimization knots + +**Files:** + +- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalKnotSchedule.cs` +- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/FullDirectionSegmentScheduleBuilder.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimitBuilder.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanningInput.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalConstraintBuilder.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPreviousTrajectorySeedBuilder.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` +- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` +- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs` + +**Interfaces:** + +- Produces immutable `LongitudinalKnotSchedule` with `KnotTimes`, `ReferencePathS`, `ReferenceSpeedMetersPerSecond`, `TotalDurationSeconds`, and `IsAdaptive`. +- Produces `FullDirectionSegmentScheduleBuilder.TryBuild(...)` returning `EmPlanningStatus.Success` or `FullSegmentResourceLimitExceeded`. +- `LongitudinalPlanningInput.PlanningScope` carries the request scope into the independent validator. +- `LongitudinalPlanningInput.KnotSchedule` becomes the only optimization-knot source. +- Publication still uses `Scheduling.OutputTimeStepSeconds`; no caller treats it as the optimization step in full mode. + +- [ ] **Step 1: Write failing duration, adaptivity, and resource-limit tests** + +Cover these deterministic fixtures: + +```csharp +// A short segment completes before a legacy 10 s horizon. +Verification.True(shortSchedule.TotalDurationSeconds < 10d, "short segment derives its own T_end"); +// A longer segment takes longer without changing DistanceHorizonMeters/TimeHorizonSeconds. +Verification.True(longSchedule.TotalDurationSeconds > shortSchedule.TotalDurationSeconds, + "duration grows from s_end and limits"); +// Curvature transitions and the stop boundary are represented in the adaptive schedule. +Verification.True(longSchedule.KnotTimes.Count <= configuration.Scheduling.MaximumOptimizationKnotCount, + "adaptive schedule respects knot cap"); +Verification.NearlyEqual(path.Points[path.Points.Count - 1].PathS, + longSchedule.ReferencePathS[longSchedule.ReferencePathS.Count - 1], "schedule reaches s_end"); +Verification.NearlyEqual(0d, + longSchedule.ReferenceSpeedMetersPerSecond[longSchedule.ReferenceSpeedMetersPerSecond.Count - 1], + "schedule stops at s_end"); +``` + +Set `MaximumOptimizationKnotCount = 4` on a path that needs more intervals and assert exact status `FullSegmentResourceLimitExceeded`, no schedule, and a diagnostic containing required versus configured knots. + +- [ ] **Step 2: Run RED** + +```powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model +``` + +Expected: compile failure for the new schedule types. + +- [ ] **Step 3: Implement the immutable schedule contract** + +Use this public shape: + +```csharp +public sealed class LongitudinalKnotSchedule +{ + public LongitudinalKnotSchedule(IReadOnlyList knotTimes, + IReadOnlyList referencePathS, + IReadOnlyList referenceSpeedMetersPerSecond, bool isAdaptive); + + public IReadOnlyList KnotTimes { get; } + public IReadOnlyList ReferencePathS { get; } + public IReadOnlyList ReferenceSpeedMetersPerSecond { get; } + public double TotalDurationSeconds { get; } + public bool IsAdaptive { get; } + + public static LongitudinalKnotSchedule CreateRolling( + double timeHorizonSeconds, double timeStepSeconds); +} +``` + +Validate equal counts, exact zero first time/path, strictly increasing times, nondecreasing PathS, finite nonnegative speed, and exact zero terminal speed for adaptive schedules. + +- [ ] **Step 4: Extract a PathSpeedLimitBuilder overload that does not require knot times** + +Add: + +```csharp +public EmPlanningStatus Build(LateralPath path, TravelDirection direction, + double initialProgressSpeedMetersPerSecond, EmTerminalType terminalType, + EmPlannerConfiguration configuration, out PathSpeedLimit speedLimit, + out string failureReason); +``` + +Keep the existing `Build(LongitudinalPlanningInput, ...)` as a delegating compatibility overload. This allows the service to build the physical PathS envelope before it creates the optimization schedule. + +- [ ] **Step 5: Implement full-segment schedule derivation** + +Use one forward/backward envelope pass over the existing `PathSpeedLimit.PathS` stations: + +1. Start with the hard maximum-speed array. +2. Forward-propagate a jerk-limited reachable speed from `v0/a0`, never exceeding desired speed or the hard PathS envelope. +3. Backward-propagate the jerk-limited stopping envelope to exact `(s_end, v=0, a=0)` using `JerkLimitedStoppingMath` for every remaining-distance check. +4. Intersect forward, backward, curvature, and desired-speed limits. +5. Integrate `dt = 2*ds/(v0+v1)` on nonzero-speed intervals; use the jerk-limited acceleration/deceleration phase duration when either endpoint speed is zero. +6. Add exact breakpoints for startup, speed-limit changes, brake onset, terminal stop, and the zero-speed hold. +7. Refine any interval exceeding either configured maximum `dt` or maximum `ds`. +8. If refinement needs more than `MaximumOptimizationKnotCount`, return `FullSegmentResourceLimitExceeded`. + +The builder signature is: + +```csharp +public EmPlanningStatus TryBuild(LateralPath path, PathSpeedLimit speedLimit, + double initialProgressSpeedMetersPerSecond, + double initialAccelerationMetersPerSecondSquared, + double desiredSpeedMetersPerSecond, EmPlannerConfiguration configuration, + out LongitudinalKnotSchedule schedule, out string failureReason); +``` + +Do not clamp a result to `s_end` after integration. A schedule is accepted only if its final reference state is exactly `s_end/0/0` within configured kinematic tolerance. + +- [ ] **Step 6: Make all ST components consume `input.KnotSchedule`** + +Add both `EmPlanningScope planningScope` and the schedule to `LongitudinalPlanningInput`, copy them into immutable `PlanningScope` and `KnotSchedule` properties, and reject an adaptive schedule supplied to rolling scope or a rolling schedule supplied to full scope. Replace every direct call to: + +```csharp +LongitudinalCandidate.CreateKnotTimes( + input.Configuration.Scheduling.TimeHorizonSeconds, + input.Configuration.Scheduling.OutputTimeStepSeconds) +``` + +with: + +```csharp +input.KnotSchedule.KnotTimes +``` + +This includes constraints, initial iterate creation, exact-stop stabilization, previous-trajectory resampling, and solution validation. The rolling service path creates `LongitudinalKnotSchedule.CreateRolling(...)`; the full path calls `FullDirectionSegmentScheduleBuilder`. + +- [ ] **Step 7: Run GREEN and prove publication sampling is independent** + +```powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration +``` + +Expected: both PASS. Add one assertion that changing `OutputTimeStepSeconds` from 0.1 to 0.05 doubles publication density but leaves `KnotSchedule.KnotTimes.Count` unchanged. + +- [ ] **Step 8: Commit adaptive scheduling** + +```powershell +git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalKnotSchedule.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/FullDirectionSegmentScheduleBuilder.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/PathSpeedLimitBuilder.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanningInput.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalConstraintBuilder.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPreviousTrajectorySeedBuilder.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs +git commit -m "feat: derive adaptive full-segment ST schedule" +``` + +--- + +### Task 4: Fix static-start progress and reject false success + +**Files:** + +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalObjectiveBuilder.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanningResult.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` +- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs` +- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs` +- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` + +**Interfaces:** + +- Consumes: desired speeds and adaptive reference profile from Tasks 1 and 3. +- Produces: a nonzero progress seed from `v0 = 0` when the segment is feasible. +- Produces: exact `NoProgress` failure with no candidate/trajectory publication. + +- [ ] **Step 1: Write the failing static-start regression** + +Use a clear 5 m forward segment, `v0 = 0`, `a0 = 0`, desired/max speed 1.0 m/s, and full scope. Assert: + +```csharp +Verification.True(result.Status == EmPlanningStatus.Success || + result.Status == EmPlanningStatus.SuccessWithFallback, "static start succeeds"); +Verification.True(result.Trajectory.Points.Any(point => point.PathS > 0.05d), + "static start makes measurable progress"); +Verification.True(result.Trajectory.Points.Any(point => point.SignedLongitudinalVelocity > 0.05d), + "static start accelerates"); +Verification.NearlyEqual(0d, result.Trajectory.Points[^1].SignedLongitudinalVelocity, + "full segment stops at the terminal boundary"); +``` + +Add a direct validator fixture containing all-zero `S/U/A/J` over a nonzero full segment. Expect `NoProgress`. Add allowed zero-progress fixtures for “already within terminal tolerance” and gear-switch stop hold. + +- [ ] **Step 2: Run RED against the original symptom** + +```powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration +``` + +Expected: the static-start assertion fails because the current envelope seed targets the initial zero speed. + +- [ ] **Step 3: Drive the seed and objective toward the feasible reference speed** + +In `CreateEnvelopeSeed`, replace the current target: + +```csharp +double targetSpeed = Math.Min(input.InitialProgressSpeedMetersPerSecond, speedLimitAtS); +``` + +with: + +```csharp +double desiredSpeed = input.Direction == TravelDirection.Forward + ? configuration.DesiredForwardSpeedMetersPerSecond + : configuration.DesiredReverseSpeedMetersPerSecond; +double scheduleSpeed = input.KnotSchedule.ReferenceSpeedMetersPerSecond[index]; +double targetSpeed = Math.Min(desiredSpeed, Math.Min(scheduleSpeed, speedLimitAtS)); +``` + +In `LongitudinalObjectiveBuilder`, track the same reference rather than only the current linearization-point hard limit. Preserve acceleration, jerk, previous-trajectory, and exact-stop terms. + +- [ ] **Step 4: Add the independent no-progress gate** + +After physical/dynamics checks and before returning a canonical candidate, reject full-scope output when: + +```csharp +bool requiresProgress = input.PathUpperBoundS > + input.Configuration.Validation.TerminalPositionToleranceMeters; +double achievedProgress = candidate.S[candidate.S.Count - 1] - candidate.S[0]; +if (input.PlanningScope == EmPlanningScope.FullDirectionSegment && + requiresProgress && achievedProgress <= input.Configuration.Validation.SpatialToleranceMeters) +{ + failureReason = "NoProgress: a nonterminal full direction segment produced zero progress."; + failureStatus = EmPlanningStatus.NoProgress; + return false; +} +``` + +Extend the validator/result seam to return a status as well as a reason so `NoProgress` is not collapsed into generic `LongitudinalInfeasible`. + +- [ ] **Step 5: Run GREEN and the service gate** + +```powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-model +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service +``` + +Expected: all PASS; the explicit all-zero service fixture has `Trajectory == null` and diagnostic `NoProgress`. + +- [ ] **Step 6: Commit the static-start fix** + +```powershell +git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalObjectiveBuilder.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalSolutionValidator.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanningResult.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalModelChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs +git commit -m "fix: accelerate EM trajectories from rest" +``` + +--- + +### Task 5: Enforce terminal world pose and publication limits + +**Files:** + +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/TrajectorySampleSchedule.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs` +- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs` +- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs` + +**Interfaces:** + +- Produces: `EmTrajectoryValidator.Validate(..., Pose2D terminalPose, ...)`. +- Produces normalized yaw helper with result in `[-π, π]`. +- Produces publication sample-count gate using `MaximumPublishedSampleCount`. +- Maps pose mismatch to `EmPlanningStatus.TerminalPoseMismatch` and sample overflow to `FullSegmentResourceLimitExceeded`. + +- [ ] **Step 1: Write failing position, yaw-wrap, and sample-limit tests** + +Create terminal trajectories at 2.9 cm and 3.1 cm position error; create yaw cases `179°` versus `-179°`, `4.9°`, and `5.1°`. Assert only the outside cases fail. Add a trajectory whose output sampling would need one sample above `MaximumPublishedSampleCount` and assert no partial trajectory is assembled. + +- [ ] **Step 2: Run RED** + +```powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory +``` + +Expected: compile failure because terminal pose is not part of validation. + +- [ ] **Step 3: Add normalized terminal-pose validation** + +Use the final Local G2 point as the expected world pose and validate the real terminal anchor: + +```csharp +private static double NormalizeAngle(double angle) +{ + while (angle > Math.PI) angle -= 2d * Math.PI; + while (angle < -Math.PI) angle += 2d * Math.PI; + return angle; +} + +double dx = terminal.X - terminalPose.X; +double dy = terminal.Y - terminalPose.Y; +double positionError = Math.Sqrt(dx * dx + dy * dy); +double yawError = Math.Abs(NormalizeAngle(terminal.Yaw - terminalPose.Heading)); +if (positionError > configuration.Validation.TerminalPositionToleranceMeters || + yawError > configuration.Validation.TerminalYawToleranceRadians) +{ + return Reject(EmTrajectoryValidationFailure.TerminalPoseMismatch, terminalIndex, + "TerminalPoseMismatch: position=" + Format(positionError) + + ";yaw=" + Format(yawError)); +} +``` + +Do this only for real `Goal` and `GearSwitchApproach` anchors, not rolling-safety window ends. + +- [ ] **Step 4: Enforce exact publication cardinality** + +Before allocating published samples, compute the required count including the exact terminal anchor and hold samples. If it exceeds `MaximumPublishedSampleCount`, return `FullSegmentResourceLimitExceeded`; do not remove the terminal anchor, increase `OutputTimeStepSeconds`, or truncate. + +Keep jerk interval semantics: the assembler uses interval `i` for point `i` only while `i + 1 < N`; the terminal point stores no successor-interval jerk and the chart builder publishes only `N-1` values. + +- [ ] **Step 5: Run GREEN** + +```powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service +``` + +Expected: both PASS, including yaw wrap and boundary-outside rejection. + +- [ ] **Step 6: Commit publication gates** + +```powershell +git add -- ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/TrajectorySampleSchedule.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs +git commit -m "feat: validate EM terminal world pose" +``` + +--- + +### Task 6: Make MovementTest one-shot per direction segment + +**Files:** + +- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs` +- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs` +- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` +- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs` + +**Interfaces:** + +- Produces: `TrajectoryObservationSettings.PlanningScope`, defaulting to `FullDirectionSegment`. +- Produces: one plan attempt per active segment, reset only after a confirmed segment transition. +- Preserves: stop hold plus three correctly signed speed samples before `N -> N+1`. + +- [ ] **Step 1: Write failing settings and cadence tests** + +Assert the validated snapshot freezes `FullDirectionSegment`, Web defaults on, Native Painter defaults off, and `OutputTimeStepSeconds == 0.1`. Drive 20 observer ticks after one successful full plan and assert planning starts once, while observation and Web publication continue on every configured cadence. Then complete the existing stop-hold/three-sample handshake and assert exactly one new plan starts for segment N+1. + +- [ ] **Step 2: Run RED** + +```powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation +``` + +Expected: failures because the current controller delegates to rolling coordinator cadence. + +- [ ] **Step 3: Add scope to settings and request creation** + +Add: + +```csharp +public EmPlanningScope PlanningScope { get; set; } = EmPlanningScope.FullDirectionSegment; +``` + +Copy and validate it. In `TrajectoryObservationController.StartCycle`, pass the scope into `EmPlanningRequest`. Apply the approved observer defaults in the MovementTest entry: + +```csharp +public bool UseFullDirectionSegmentPlanning = true; +public double OutputTimeStepSeconds = 0.10d; +public bool EnableWebVisualization = true; +public bool EnableNativePainterVisualization = false; +``` + +Keep `TimeHorizonSeconds` visible only as a rolling compatibility field and label it accordingly. Do not preserve the experimental 20 s/500 m values as the mechanism for full planning. + +- [ ] **Step 4: Gate one-shot planning in the controller** + +Track `plannedSegmentIndex` and `planAttemptedForActiveSegment` in the controller: + +```csharp +public bool ShouldStartCycle(DateTimeOffset now) +{ + if (settings.PlanningScope == EmPlanningScope.FullDirectionSegment) + return !planAttemptedForActiveSegment; + return coordinator.ShouldStartCycle(now); +} +``` + +Set the attempt flag before launching the task so a slow solver cannot trigger duplicates. Reset it only after `TryAdvanceSegment` confirms the real transition or when a new MovementTest session constructs a new controller. A failed full plan remains failed and visible; no automatic rolling retry occurs. + +- [ ] **Step 5: Keep the existing gear-switch safety state machine unchanged** + +Run existing stop-hold and signed-speed tests plus the new one-shot assertions. The waiting notice must remain explicit and the active highlight must remain on segment N until all conditions pass. + +- [ ] **Step 6: Run GREEN and source audit** + +```powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation +rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed|SetGear|SetBrake" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest +``` + +Expected: `PASS trajectory-observation`; `rg` returns no actuator call in observer runtime sources. + +- [ ] **Step 7: Commit one-shot observation** + +```powershell +git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs +git commit -m "feat: observe one full EM direction segment" +``` + +--- + +### Task 7: Publish unambiguous path, chart, and boundary semantics + +**Files:** + +- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationCharts.cs` +- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationGeometry.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs` +- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs` +- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs` +- Test: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs` +- Test: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs` + +**Interfaces:** + +- Produces: `VisualizationChartAnnotation` for vertical `s_end` lines and labeled points. +- Produces marker kinds `plan-start`, `gear-switch-end`, and `final-goal`. +- Produces distinct polyline kinds `coarse`, `local-g2`, `active-segment`, `previous`, and `current` with no duplicate `current-horizon` in full mode. +- Chart axes remain `ReferenceS (m)`, `PathS (m)`, `t (s)`, `l (m)`, `v (m/s)`, `a (m/s²)`, `j (m/s³)`, `κ (m⁻¹)`, and `ω (rad/s)`. + +- [ ] **Step 1: Write failing snapshot semantics tests** + +Assert that a full-mode snapshot contains exactly one current trajectory polyline, contains both coarse and Local G2 paths, has distinct `s_end` and vehicle/plan-start markers, and uses the correct terminal kind for gear switch versus final goal. + +For charts assert: + +```csharp +Verification.Equal("ReferenceS (m)", ls.XAxisLabel, "LS uses shared reference S"); +Verification.Equal("PathS (m)", st.YAxisLabel, "ST uses actual path arc length"); +Verification.Equal(trajectory.Points.Count - 1, jerk.Series[0].Points.Count, + "jerk has N-1 interval samples"); +Verification.True(st.Annotations.Any(a => a.Kind == "s-end"), "ST marks s_end"); +Verification.True(curvatureS.Series[0].Points.Any(p => Math.Abs(p.Y) > 1e-6), + "curvature-distance uses vehicle curvature data"); +``` + +Ensure the curvature hard-limit series uses `VehicleParameters.MaximumCurvaturePerMeter`, not `MaximumCurvatureRatePerMeterPerSecond`. + +- [ ] **Step 2: Run RED** + +```powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation +dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj +``` + +Expected: failures for duplicate current horizon, missing annotations, and incorrect curvature limit. + +- [ ] **Step 3: Add chart annotations without breaking old consumers** + +Add: + +```csharp +public sealed class VisualizationChartAnnotation +{ + public VisualizationChartAnnotation(string id, string kind, string labelChinese, + double x, double? y = null); + public string Id { get; } + public string Kind { get; } + public string LabelChinese { get; } + public double X { get; } + public double? Y { get; } +} +``` + +Extend `VisualizationChart` with immutable `Annotations`. Keep the old constructor as a delegating overload with an empty annotation list for backward compatibility. + +- [ ] **Step 4: Correct snapshot builders** + +In full mode, remove `current-horizon`; in rolling mode publish it only if it is a genuinely different subset. Add current trajectory once above static paths. Add `s_end` annotations to world, LS, and ST. Label a direction boundary as `换向点 N / s_end` only for gear switches; use a distinct final-goal label and marker. + +Extend `TrajectoryObservationDynamicSnapshotBuilder.Build(...)` with an immutable `VehicleParameters vehicle` argument. Pass `bootstrap.Vehicle` from the MovementTest publication callback into the dynamic builder, then into the kinematic chart builder, so the curvature charts use the real vehicle curvature limit. Change yaw-rate axis text to `ω (rad/s)`. + +- [ ] **Step 5: Run GREEN** + +Run both commands from Step 2. Expected: both hosts PASS. + +- [ ] **Step 6: Commit observation semantics** + +```powershell +git add -- ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationCharts.cs ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationGeometry.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs +git commit -m "fix: publish distinct EM observation semantics" +``` + +--- + +### Task 8: Add real DOM/SVG tests and repair the existing Web page + +**Files:** + +- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package.json` +- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package-lock.json` +- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs` +- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html` +- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css` +- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js` +- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs` + +**Interfaces:** + +- Test-only dependency: exact `jsdom` 29.1.1; no npm package is copied into the production plugin. +- Test hook: when `window.__TRAJECTORY_VISUALIZATION_TEST__ === true`, `app.js` exposes immutable-input render hooks and does not start fetch/SSE. +- Produces actual DOM assertions for tab visibility, y ticks, units, paths, markers, empty state, and scientific line classes. + +- [ ] **Step 1: Add the test package and failing DOM tests** + +Create `package.json`: + +```json +{ + "name": "trajectory-planning-visualization-web-dom-tests", + "private": true, + "type": "module", + "scripts": { "test": "node --test dashboard.dom.test.mjs" }, + "devDependencies": { "jsdom": "29.1.1" }, + "engines": { "node": ">=24" } +} +``` + +Run `npm install --package-lock-only` in that directory to create the lock file. In `dashboard.dom.test.mjs`, load the real `index.html` and `app.js`, create `JSDOM` with `runScripts: "outside-only"` and `pretendToBeVisual: true`, stub Canvas `getContext`, set fixed element rectangles, set the test flag, and evaluate the real script. + +Test these behaviors with actual DOM queries: + +```javascript +assert.equal(document.querySelector("#overview").hidden, false); +click(document.querySelector('button[data-tab="ls-st"]')); +assert.equal(document.querySelector("#overview").hidden, true); +assert.equal(document.querySelector("#ls-st").hidden, false); +assert.equal(document.querySelector("#kinematics").hidden, true); +assert.ok(document.querySelectorAll("#st .axis-tick-y").length >= 4); +assert.equal(document.querySelector("#st .axis-label-y").textContent, "PathS (m)"); +assert.ok(document.querySelector("#world-overlay .world-current")); +assert.ok(document.querySelector("#world-overlay .marker-vehicle")); +``` + +Add an empty-snapshot test expecting a visible Chinese reason instead of an empty world canvas. + +- [ ] **Step 2: Run RED** + +```powershell +npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom +``` + +Expected: failures because hidden sections are overridden by CSS, y tick labels are absent, and semantic classes/empty messages are missing. + +- [ ] **Step 3: Fix tab visibility and empty-state behavior** + +Add the high-specificity CSS rule before panel grids: + +```css +main > section[hidden] { display: none !important; } +#ls-st:not([hidden]), #kinematics:not([hidden]) { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); + gap: 18px; +} +``` + +In `renderWorld`, count valid static/dynamic path points. If none exist, add a `.world-empty-state` text element naming the missing source. Do not fabricate fallback points. + +- [ ] **Step 4: Render y ticks, units, and stable nonzero domains** + +Split tick generation into `niceDomain`, `formatTick`, and `renderAxes`. Create both x and y text nodes: + +```javascript +const yt = svg("text", { + x: left - 7, y, class: "axis-tick axis-tick-y", + "text-anchor": "end", "dominant-baseline": "middle" +}); +yt.textContent = formatTick(domain.y1 - tick * (domain.y1 - domain.y0) / 4); +``` + +Use `.axis-label-x` and `.axis-label-y` classes. For constant data, pad around the actual constant with `max(abs(value)*0.05, unitFloor)`; never force a nonzero constant onto a zero-centered 1-unit range. + +- [ ] **Step 5: Apply semantic path order and thin paper style** + +Dispatch line class by `kind`, not by static/dynamic origin. Draw in this exact order: coarse, Local G2, direction segments, previous, current, annotations/vehicle. Use: + +```css +.world-coarse { stroke: #9aa1a8; stroke-opacity: .48; stroke-dasharray: 5 4; stroke-width: .85; } +.world-local-g2 { stroke: #4f5963; stroke-width: .95; } +.world-segment-active { stroke: #5d91bd; stroke-opacity: .62; stroke-width: 1.0; } +.world-previous { stroke: #8d959d; stroke-dasharray: 5 4; stroke-width: .85; } +.world-current { stroke: #1769aa; stroke-width: 1.15; } +``` + +Render vehicle pose as a small oriented outline plus thin heading line using `frame.vehiclePose.heading`; render plan start, gear-switch `s_end`, and final goal with different marker classes. Do not use a large filled triangle. + +- [ ] **Step 6: Expose safe test hooks and keep production boot unchanged** + +At the end of `app.js`: + +```javascript +if (window.__TRAJECTORY_VISUALIZATION_TEST__ === true) { + window.__trajectoryVisualizationTestHooks = Object.freeze({ + setBootstrap(snapshot) { state.staticSnapshot = snapshot; }, + receiveFrame, + renderActiveTab, + installTabs + }); + installTabs(); +} else { + boot(); +} +``` + +Hooks accept snapshots but never expose mutable internal state. Production still performs tokenized fetch and SSE exactly once. + +- [ ] **Step 7: Run GREEN plus embedded-asset host** + +```powershell +npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom +dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj +``` + +Expected: Node tests report all tests passed; .NET host prints `PASS trajectory-planning-visualization`. + +- [ ] **Step 8: Commit Web repair and DOM tests** + +```powershell +git add -- ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package.json ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package-lock.json ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs +git commit -m "fix: repair EM observation web charts" +``` + +--- + +### Task 9: Add per-chart viewport zoom without changing data + +**Files:** + +- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html` +- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css` +- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js` +- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs` + +**Interfaces:** + +- Produces per-chart `chartViewports[id]` containing only visible x/y domains. +- Produces actions `box zoom`, `wheel zoom`, `reset`, and `fullscreen`. +- Consumes immutable chart snapshot arrays and never writes to them. + +- [ ] **Step 1: Write failing interaction tests** + +Freeze the input chart and point arrays, dispatch wheel and pointer events, and assert the rendered axis tick labels and path coordinates change while the serialized input remains byte-for-byte identical. Click reset and assert the original tick domain returns. Click fullscreen and assert only the selected `.chart` receives the fullscreen class/API request. + +- [ ] **Step 2: Run RED** + +```powershell +npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom +``` + +Expected: zoom controls and viewport state are missing. + +- [ ] **Step 3: Implement viewport-only controls** + +Add a compact `.chart-tools` toolbar in each chart host. Store domains as copied numbers: + +```javascript +state.chartViewports[id] = { x0, x1, y0, y1 }; +``` + +Wheel zoom uses the pointer’s data coordinate as its anchor. Box zoom ignores drags shorter than 4 CSS pixels. Reset deletes only `state.chartViewports[id]`. Fullscreen uses `requestFullscreen()` with a CSS fallback for the jsdom/test environment. SSE frame arrival re-renders using the active viewport if it is still finite; it does not alter the snapshot. + +- [ ] **Step 4: Run GREEN** + +```powershell +npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom +dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj +``` + +Expected: all DOM tests and embedded-asset checks PASS. + +- [ ] **Step 5: Commit chart navigation** + +```powershell +git add -- ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs +git commit -m "feat: add observation chart viewport zoom" +``` + +--- + +### Task 10: Correct Native Painter geometry and semantics + +**Files:** + +- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs` +- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` +- Test: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs` + +**Interfaces:** + +- Consumes the same coarse/Local G2/current/previous/s_end semantics from Task 7. +- Produces small vehicle/pose outlines and thin heading rays with `endArrow: false`. +- LS x-axis is `ReferenceS (m)`; ST is `t (s)` versus `PathS (m)`; all world coordinates remain x/y equal-scale millimetres. + +- [ ] **Step 1: Write failing source/geometry checks** + +Add checks that `DrawPose` no longer calls `endArrow: true`, that pose outlines contain four rotated vehicle corners, and that world paths use one millimetres-per-metre factor for both x and y. Assert LS/ST labels and `s_end`/gear-switch labels match the Web semantics. + +- [ ] **Step 2: Run RED** + +```powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation +``` + +Expected: failure on the current large arrow and old `path-S`/`T-S` labels. + +- [ ] **Step 3: Replace the black triangle with scaled pose geometry** + +Draw a four-corner outline based on the observed vehicle length/width and a thin heading ray no longer than half the vehicle length: + +```csharp +worldPainter.DrawLine(color, x, y, headingX, headingY, endArrow: false, width: 1); +``` + +Use thin line widths for coarse, Local G2, current, and previous paths. Mark current vehicle, plan start, gear-switch `s_end`, and final goal distinctly. Keep all existing Painter windows and lifecycle cleanup; do not add new Painter layers. + +- [ ] **Step 4: Correct Painter LS/ST axes** + +Use shared world `ReferenceS` for LS horizontal values and actual trajectory `PathS` for ST vertical values. Calculate plot scales from data bounds with the same x/y unit conversion. Show the explicit waiting notice during gear handoff. + +- [ ] **Step 5: Run GREEN** + +```powershell +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation +``` + +Expected: `PASS trajectory-observation`. + +- [ ] **Step 6: Commit Painter correctness** + +```powershell +git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs +git commit -m "fix: correct EM observation painter geometry" +``` + +--- + +### Task 11: Update operator documentation and run complete acceptance + +**Files:** + +- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md` +- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md` +- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/README.md` +- Create after safe execution: `docs/superpowers/handoffs/em-observation-web/phase-08-vehicle.md` + +**Interfaces:** + +- Documents the full-versus-rolling switch, derived `s_end/T_end`, 1.0/0.5 m/s limits, terminal pose tolerances, chart units, zoom, and `OBSERVE_ONLY` safety boundary. +- Produces fresh automated evidence and either a passed or explicitly pending/blocked vehicle checklist. + +- [ ] **Step 1: Update the three READMEs** + +Document these exact operator facts: + +- `FullDirectionSegment` is the MovementTest default and performs one optimization per active direction segment. +- `DistanceHorizonMeters` and `TimeHorizonSeconds` apply only to `RollingHorizon` truncation. +- `s_end` comes from actual Local G2 `PathS`; `T_end` is derived from feasible acceleration/cruise/stopping behavior. +- Forward desired/max is 1.0 m/s; reverse desired/max is 0.5 m/s. +- Successful real boundaries require stop plus 3 cm position and 5° normalized yaw tolerances. +- Web is primary, Native Painter is optional correctness/audit output. +- Each chart’s axes/units, the `N-1` jerk rule, semantic path line styles, boundary markers, and zoom controls. +- Browser closure does not stop observation; MovementTest stop reclaims HTTP/SSE/port/Painter. +- No part of MovementTest writes chassis, steering, brake, motor, or gear commands. + +- [ ] **Step 2: Run all fresh automated verification** + +```powershell +npm test --prefix ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom +dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation +dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-all +dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true +rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed|SetGear|SetBrake" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest +git diff --check +``` + +Expected: Node tests pass; both visualization and observation hosts print PASS; every `em-all` component prints PASS; build exits 0; actuator audit has no observer-runtime matches; `git diff --check` reports no new whitespace errors. Record pre-existing warnings separately. + +- [ ] **Step 3: Run deterministic local Web smoke** + +```powershell +dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -- --smoke-seconds 60 +``` + +Open the printed tokenized URL. Verify the four tabs, nonblank world overview, nonhorizontal increasing ST for the moving fixture, y ticks/units, thin path comparison, `s_end`/gear/final markers, vehicle pose, chart zoom/reset/fullscreen, stale state, and port release. This smoke uses synthetic snapshots only. + +- [ ] **Step 4: Run the supervised vehicle checklist only when safe** + +In a supervised safe environment, keep MovementTest read-only and verify: + +1. Web enabled, Native Painter disabled by default, and UI/console/page show `OBSERVE_ONLY`. +2. The current full direction segment produces complete LS/ST; static start accelerates, reaches feasible cruise speed, begins braking from the envelope, and stops at the real boundary. +3. ST `PathS` increases; jerk has `N-1` intervals and no terminal successor or false `JerkLimitExceeded`. +4. World handoff displays `DeltaPosition`, shared-direction `DeltaReferenceS`, `DeltaV`, and `DeltaA`, never a direct comparison of unrelated local PathS. +5. At a true reversal, N remains highlighted until continuous stop hold and three correctly signed speed samples pass; waiting text is explicit. +6. Coarse, Local G2, current trajectory, vehicle pose, `s_end`, gear switch, and final goal are visually distinct. +7. Closing/slow-loading the browser does not stop observation. +8. Stopping MovementTest releases HTTP/SSE, port, and any Painter; source/log audit shows no hardware writes. + +If any item cannot be executed safely, record it as not run. Do not claim overall vehicle acceptance. + +- [ ] **Step 5: Write the vehicle handoff from fresh evidence** + +If every vehicle item passes, create `phase-08-vehicle.md` with status “完成”, environment prerequisites, execution time, per-item results, logs/screenshots, warnings, and conclusion. If a regression appears, create the same file with status “阻塞”, the first valid failure, exact reproduction, and safety impact; do not expand the fix in the vehicle session. + +- [ ] **Step 6: Commit docs and, only when available, vehicle evidence separately** + +```powershell +git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md ClumsyPilot/TrajectoryPlanningVisualization/README.md +git commit -m "docs: explain full-direction EM observation" +``` + +After actual supervised acceptance only: + +```powershell +git add -- docs/superpowers/handoffs/em-observation-web/phase-08-vehicle.md +git commit -m "docs: record EM visualization vehicle acceptance" +``` + +Do not create or commit the vehicle handoff as “完成” from automated or synthetic evidence alone.