# 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 -- <path>` 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<double> knotTimes,
        IReadOnlyList<double> referencePathS,
        IReadOnlyList<double> referenceSpeedMetersPerSecond, bool isAdaptive);

    public IReadOnlyList<double> KnotTimes { get; }
    public IReadOnlyList<double> ReferencePathS { get; }
    public IReadOnlyList<double> 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.
