# EM Observation Web Visualization Integration 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:** Upgrade `TrajectoryObservationMovementTest` into a complete multi-segment, observe-only EM planning experiment that publishes effective configuration, global/active path geometry, LS/ST and kinematic evidence to the reusable local scientific dashboard.

**Architecture:** A pure segment tracker advances only after a real stop and stable signed-speed confirmation. The controller owns one coordinator per active direction segment, while pure snapshot builders convert existing EM/map/smoothing data into the generic visualization contracts. A thin MovementTest host conditionally owns web and Painter sessions and isolates every visualization failure from planning.

**Tech Stack:** C# 10, `netstandard2.0`, existing EMPlanner/TrajectoryExecution/Map/PathSmoothing modules, `TrajectoryPlanningVisualization`, OSQP, MDCS read interfaces, existing console verification host.

## Prerequisite

Complete and review `docs/superpowers/plans/2026-08-05-trajectory-planning-visualization-library.md` first. This plan consumes its exact `PlanningVisualizationSession`, snapshot, chart, geometry, and configuration contracts.

## Global Constraints

- Continue `OBSERVE_ONLY`; do not call chassis, motor, steering, brake, wheel, gear, or controller write APIs.
- Use actual MDCS pose and signed longitudinal speed only as read inputs.
- Preserve user defaults exactly: solver timeout `5.0 s`, OSQP maximum iterations `100000`, ST horizon `2.0 s`, output step `0.10 s`.
- Default web visualization is off; default native Painter visualization is off.
- Web refresh defaults to `10 Hz`; history defaults to `60`; port defaults to `0`.
- Segment transitions are strictly `N -> N+1`; never skip, infer at zero speed, or reuse a previous-direction trajectory as an EM seed.
- Current and previous cycles may compare world position and shared direction-segment `ReferenceS`; never compare their independent local `PathS` origins.
- `j[i]` represents `[t_i, t_i+1)`; publish exactly `trajectory.Points.Count - 1` jerk samples and no synthetic terminal sample.
- Static geometry/configuration is built once after successful bootstrap. Dynamic snapshots are built at most at `WebRefreshRateHz`.
- Visualization exceptions are logged once and disable only the web output.
- Preserve unrelated worktree changes and stage only task files.

---

## File Structure

| File | Responsibility |
| --- | --- |
| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs` | Runtime, web and direction-confirmation settings with frozen validation. |
| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationSegmentTracker.cs` | Pure stop/direction/projection state machine. |
| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs` | Active-segment controller and asynchronous loop integration. |
| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs` | Map/global paths/direction segments/effective configuration. |
| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs` | LS/ST/curvature/v/a/j/yaw-rate generic charts. |
| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationHandoffAnalyzer.cs` | Absolute-time interpolation and shared-segment handoff deltas. |
| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs` | World overlays, status, charts and cycle summary. |
| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationVisualizationPublisher.cs` | 10 Hz gating and exception fuse. |
| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs` | Public fields, lifecycle, browser launch and conditional Painter/web ownership. |
| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs` | Lazily created optional native Painter fallback. |
| `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md` | Operator workflow and chart interpretation. |
| `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs` | Defaults, validation and frozen-setting checks. |
| `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs` | Pure multi-segment transition checks. |
| `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs` | Static/dynamic adapter and web isolation checks. |
| `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs` | Existing regression entry; calls the new focused groups. |
| `ClumsyPilot/tests/EMPlannerVerificationHost/PluginPackagingChecks.cs` | New managed DLL packaging contract. |
| `ClumsyPilot/scripts/Publish-ClumsyPilotPlugin.ps1` | Copies the visualization class library beside `ClumsyPilot.dll`. |
| `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md` | Updated plugin tree and observation link. |

---

### Task 1: Synchronize effective settings and visualization controls

**Files:**

- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs`
- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md`

**Interfaces:**

- Extends `TrajectoryObservationSettings` with web/Painter and direction-confirmation properties.
- `CreateValidatedSnapshot()` freezes every new property.
- MovementTest public fields map one-to-one into settings before background work begins.

- [ ] **Step 1: Write failing defaults, copy, and validation checks**

Add `TrajectoryObservationSettingsChecks.Run()` and invoke it from `TrajectoryObservationChecks.Run()`.

```csharp
var defaults = new TrajectoryObservationSettings();
Verification.NearlyEqual(5d, defaults.SolverTimeoutSeconds, "observer solver timeout default");
Verification.Equal(100000, defaults.MaximumOsqpIterations, "observer OSQP default");
Verification.NearlyEqual(2d, defaults.TimeHorizonSeconds, "observer ST horizon default");
Verification.True(!defaults.EnableWebVisualization, "web defaults off");
Verification.True(!defaults.EnableNativePainterVisualization, "Painter defaults off");
Verification.Equal(0, defaults.WebVisualizationPort, "dynamic port default");
Verification.NearlyEqual(10d, defaults.WebRefreshRateHz, "web refresh default");
Verification.Equal(60, defaults.VisualizationHistoryCycleLimit, "history default");
Verification.NearlyEqual(0.02d, defaults.DirectionConfirmationSpeedMetersPerSecond,
    "direction speed threshold");
Verification.Equal(3, defaults.DirectionConfirmationSamples, "direction sample default");
Verification.NearlyEqual(0.50d, defaults.GearSwitchProjectionToleranceMeters,
    "gear projection default");
Verification.NearlyEqual(0.20d, defaults.GearSwitchStopHoldSeconds, "gear stop hold default");
```

Mutate all source values after `CreateValidatedSnapshot()` and assert the snapshot retains the originals. Add invalid cases for port `-1/1/1023/65536`, refresh `0`, history `0`, speed threshold `0`, sample count `0`, projection tolerance `0`, and hold `0`.

- [ ] **Step 2: Run RED**

```powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
```

Expected: failure because settings still use `0.50 / 12000 / 6.0` and the new properties do not exist.

- [ ] **Step 3: Implement settings and MovementTest field mapping**

Add exact get/set defaults to settings and matching public fields to MovementTest:

```csharp
public bool EnableWebVisualization { get; set; } = false;
public bool AutoOpenWebVisualization { get; set; } = true;
public int WebVisualizationPort { get; set; } = 0;
public double WebRefreshRateHz { get; set; } = 10d;
public int VisualizationHistoryCycleLimit { get; set; } = 60;
public bool EnableNativePainterVisualization { get; set; } = false;
public double DirectionConfirmationSpeedMetersPerSecond { get; set; } = 0.02d;
public int DirectionConfirmationSamples { get; set; } = 3;
public double GearSwitchProjectionToleranceMeters { get; set; } = 0.50d;
public double GearSwitchStopHoldSeconds { get; set; } = 0.20d;
```

Use inclusive validation for port `0` and `1024..65535`. Update the Chinese XML summaries so the ST default says `2 s`, not `6 s`. Update the README configuration table with all solver, ST, web, Painter and direction-confirmation fields.

- [ ] **Step 4: Run GREEN**

Run `trajectory-observation`. Expected: PASS and no assertion still expects the superseded defaults.

- [ ] **Step 5: Commit settings**

```powershell
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: configure observation visualization"
```

---

### Task 2: Pure sequential direction-segment tracker

**Files:**

- Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationSegmentTracker.cs`
- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs`

**Interfaces:**

- Produces: `TrajectoryObservationSegmentPhase`, `TrajectoryObservationSegmentState`, and `TrajectoryObservationSegmentTracker.Update(...)`.
- Consumes: frozen settings, ordered `DirectionSegmentView` list, EM stop tolerance, caller time, measured `VehicleMotionState`, and current published trajectory.

- [ ] **Step 1: Write failing state-machine checks**

Build a two-segment forward/reverse fixture with a duplicated world pose at the gear switch. Exercise this exact sequence:

```csharp
var tracker = new TrajectoryObservationSegmentTracker(segments, settings, stopSpeedTolerance: 0.01d);
Verification.Equal(0, tracker.State.ActiveSegmentIndex, "tracker begins on segment zero");

tracker.Update(t0, StateAtSwitch(0d, t0, 1L), GearTerminal(t0));
Verification.Equal(TrajectoryObservationSegmentPhase.WaitingForStop, tracker.State.Phase,
    "first zero sample begins stop hold");

tracker.Update(t0.AddSeconds(0.21d), StateAtSwitch(0d, t0.AddSeconds(0.21d), 2L), GearTerminal(t0));
Verification.Equal(TrajectoryObservationSegmentPhase.WaitingForDirection, tracker.State.Phase,
    "continuous stop arms next direction");

tracker.Update(t0.AddSeconds(0.25d), StateAtSwitch(-0.03d, t0.AddSeconds(0.25d), 3L), GearTerminal(t0));
tracker.Update(t0.AddSeconds(0.30d), StateAtSwitch(-0.03d, t0.AddSeconds(0.30d), 4L), GearTerminal(t0));
TrajectoryObservationSegmentUpdate advanced = tracker.Update(
    t0.AddSeconds(0.35d), StateAtSwitch(-0.03d, t0.AddSeconds(0.35d), 5L), GearTerminal(t0));
Verification.True(advanced.Advanced && tracker.State.ActiveSegmentIndex == 1,
    "three stable reverse samples advance exactly one segment");
```

Separate checks prove that wrong sign, a zero sample, a sequence-id repeat, excessive switch distance, a non-gear terminal, and a discontinuous timestamp reset confirmation. A one-segment path reaches `Completed` rather than indexing past the end.

- [ ] **Step 2: Run RED**

Run `trajectory-observation`. Expected: compile failure because tracker types do not exist.

- [ ] **Step 3: Implement the tracker**

Use these stable phases:

```csharp
public enum TrajectoryObservationSegmentPhase
{
    Planning,
    WaitingForStop,
    WaitingForDirection,
    Completed,
}
```

`Update` first validates strictly increasing `SequenceId` and nondecreasing caller time. It only leaves `Planning` when the current published trajectory has matching segment/direction, `TerminalType.GearSwitch`, and the caller time is at or beyond `trajectory.Metadata.EffectiveAtUtc + trajectory.Points[^1].TimeFromStart`. Use `FrenetProjector` to confirm the measured pose is within the configured tolerance of both the current-segment end and next-segment start.

The expected speed sign is positive for `Forward` and negative for `Reverse`. A successful update changes the active index once, resets counters/timers, and returns a Chinese transition diagnostic. It must not write to coordinator, browser, UI, or hardware.

- [ ] **Step 4: Run GREEN**

Run `trajectory-observation` twice. Expected: PASS both times with deterministic state transitions.

- [ ] **Step 5: Commit tracker**

```powershell
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationSegmentTracker.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: track observed direction segments"
```

---

### Task 3: Active-segment rolling controller and loop

**Files:**

- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs`

**Interfaces:**

- `TrajectoryObservationController.ActiveSegment`, `SegmentState`, `PreviousTrajectoryForVisualization`, and `TryAdvanceSegment(...)` become the single active-segment source.
- `TrajectoryObservationController.CreateEffectiveConfigurationSnapshot()` returns `configuration.Copy()` so adapters never read mutable MovementTest fields or retain the controller's private configuration object.
- `StartCycle` builds a request for `ActiveSegment.SegmentIndex`.
- `TrajectoryObservationLoop.Tick` may call `TryAdvanceSegment` only when no planning task is in flight.

- [ ] **Step 1: Write failing controller integration checks**

Use a recording `IEmPlanningService` and two-segment bootstrap:

```csharp
controller.StartCycle(t0, forwardState, CancellationToken.None).GetAwaiter().GetResult();
Verification.Equal(0, service.Requests[0].SegmentIndex, "first request uses segment zero");

AdvanceTrackerThroughRealStopAndReverse(controller, gearTrajectory, t0);
controller.StartCycle(t0.AddSeconds(1d), reverseState, CancellationToken.None).GetAwaiter().GetResult();
Verification.Equal(1, service.Requests[1].SegmentIndex, "next request uses segment one");
Verification.True(service.Requests[1].PreviousTrajectory == null,
    "new direction does not reuse old segment trajectory");
```

Also prove two cycles on the same segment retain the exact published previous trajectory reference and ID, and prove an in-flight task prevents a segment reset.

- [ ] **Step 2: Run RED**

Expected: checks fail because the controller hardcodes `segmentIndex = 0`.

- [ ] **Step 3: Refactor controller ownership**

Store the planning service and make coordinator/executor replaceable per segment:

```csharp
private readonly IEmPlanningService planningService;
private EmPlanningCoordinator coordinator;
private TrajectoryExecutor executor;
private readonly TrajectoryObservationSegmentTracker segmentTracker;
private EmTrajectory previousTrajectoryForVisualization;
```

Add:

```csharp
internal EmPlannerConfiguration CreateEffectiveConfigurationSnapshot()
{
    return configuration.Copy();
}
```

On a confirmed transition, preserve the old published trajectory only in `previousTrajectoryForVisualization`, then construct a new coordinator and executor. Same-segment `StartCycle` uses `coordinator.PublishedTrajectory`; cross-segment starts with null. Continue monotonic session cycle IDs across coordinator replacement.

`Observe` supplies the tracker-confirmed current direction to `TrajectoryExecutor`. While waiting at a gear switch, desired direction is the next segment direction and `directionConfirmed=false`; after transition both current and desired are the new direction.

- [ ] **Step 4: Update the async loop**

In `Tick`, after consuming a completed task and before starting a new task:

```csharp
bool segmentAdvanced = planningTask == null && controller.TryAdvanceSegment(now, state);
if (segmentAdvanced)
    latestCycle = null;
```

Expose `SegmentAdvanced` and the immutable segment state in `TrajectoryObservationLoopTick`. Never cancel a running planner solely to advance a segment.

- [ ] **Step 5: Run GREEN and existing coordinator/executor regressions**

```powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor
```

Expected: all three PASS.

- [ ] **Step 6: Commit controller integration**

```powershell
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSegmentChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: observe EM planning across gear segments"
```

---

### Task 4: Static world geometry and effective configuration adapter

**Files:**

- Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs`
- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs`

**Interfaces:**

- Produces: `PlanningVisualizationStaticSnapshot Build(bootstrap, effectiveConfiguration, settings, obstacleSnapshotVersion)`.
- Static polylines use metres; map source bounds/resolution remain named with their original millimetre units in configuration values.

- [ ] **Step 1: Write failing static snapshot checks**

Assert a forward/reverse bootstrap produces every segment and switch marker:

```csharp
PlanningVisualizationStaticSnapshot snapshot = builder.Build(bootstrap, configuration, settings, 17L);
Verification.Equal(bootstrap.Segments.Count, snapshot.DirectionSegments.Count,
    "all direction segments exported");
Verification.Equal("Forward", snapshot.DirectionSegments[0].Direction, "forward direction exported");
Verification.Equal("Reverse", snapshot.DirectionSegments[1].Direction, "reverse direction exported");
Verification.True(snapshot.StaticMarkers.Any(x => x.Kind == "gear-switch"),
    "gear switch marker exported");
Verification.Equal((bootstrap.GridMap.Rows * bootstrap.GridMap.Cols + 7) / 8,
    Convert.FromBase64String(snapshot.OccupancyGrid.OccupancyBitsBase64).Length,
    "occupancy bitset has exact compact length");
Verification.True(IsOccupiedBitSet(snapshot.OccupancyGrid, occupiedRow, occupiedColumn),
    "occupied map cell uses row-major least-significant-bit-first encoding");
```

Flatten configuration entries by raw field name and assert exact values for `TimeHorizonSeconds=2`, `DistanceHorizonMeters=5`, `MaximumOsqpIterations=100000`, all solver tolerances, all LS/ST weights, vehicle fields, map snapshot/resolution/bounds, web settings, and direction-confirmation settings.

- [ ] **Step 2: Run RED**

Expected: compile failure because builder does not exist.

- [ ] **Step 3: Implement static conversion**

Build configuration groups in this stable order: `调度`, `OSQP`, `车辆与安全`, `纵向限制`, `ST 权重`, `横向限制`, `LS 权重`, `走廊与投影`, `地图`, `可视化与换向确认`.

Use raw field names exactly as C# properties and invariant values. Encode `PlanningGridMap.IsOccupied(row, column)` into the generic row-major occupancy bitset using bit index `row * map.Cols + column`; do not create one DTO, SVG element, or polyline per occupied cell. Build global coarse and Local G2 polylines once. Direction segments must retain segment index, direction and switch topology; dynamic state decides completed/current/future highlighting.

- [ ] **Step 4: Run GREEN**

Run `trajectory-observation`. Expected: PASS with all effective values read from the controller's frozen `EmPlannerConfiguration`, not editable MovementTest fields.

- [ ] **Step 5: Commit static adapter**

```powershell
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationStaticSnapshotBuilder.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: export EM observation configuration"
```

---

### Task 5: Kinematic charts, rolling semantics, and handoff evidence

**Files:**

- Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationHandoffAnalyzer.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs`

**Interfaces:**

- Produces chart IDs `ls`, `st`, `curvature-s`, `curvature-t`, `velocity-t`, `acceleration-t`, `jerk-t`, `yaw-rate-t`.
- Produces `TrajectoryObservationHandoffMetrics` with availability, `DeltaPositionMeters`, `DeltaReferenceSMeters`, `DeltaVelocityMetersPerSecond`, and `DeltaAccelerationMetersPerSecondSquared`.
- Produces one `PlanningVisualizationDynamicSnapshot` per accepted publication tick.

- [ ] **Step 1: Write failing jerk and chart checks**

For a 21-point rolling trajectory:

```csharp
IReadOnlyList<VisualizationChart> charts = chartBuilder.Build(trajectory, segment, configuration);
VisualizationChart jerk = FindChart(charts, "jerk-t");
Verification.Equal(20, jerk.Series[0].Points.Count, "jerk has one real sample per interval");
Verification.True(jerk.NoteChinese.Contains("末点后无时间区间"), "jerk explains terminal interval");
Verification.Equal(21, FindChart(charts, "velocity-t").Series[0].Points.Count,
    "velocity remains knot based");
Verification.Equal("ReferenceS (m)", FindChart(charts, "ls").XAxisLabel,
    "LS uses shared segment station");
Verification.Equal("PathS (m)", FindChart(charts, "st").YAxisLabel,
    "ST uses local planned PathS");
```

Assert acceleration, jerk, curvature and signed-speed limit series are present with `VisualizationLineStyle.Limit`. Do not invent a yaw-rate limit: the effective configuration has no independent maximum yaw-rate field.

- [ ] **Step 2: Write failing rolling/exact-stop semantic checks**

Build dynamic snapshots for `RollingContinuation` and `ExactStopAtBoundary`. Assert Chinese status values respectively contain `滚动末端停车硬约束：未启用` and `精确停车锚点`, and that the displayed terminal speed/acceleration equal the actual final point.

- [ ] **Step 3: Write failing handoff coordinate checks**

Create two trajectories whose local `PathS` both start at zero but whose world points project to different positions on the same full segment. Align at `current.Metadata.EffectiveAtUtc`; assert:

```csharp
Verification.NearlyEqual(expectedWorldDelta, metrics.DeltaPositionMeters, "handoff world delta");
Verification.NearlyEqual(expectedReferenceDelta, metrics.DeltaReferenceSMeters,
    "handoff compares shared ReferenceS");
Verification.True(expectedReferenceDelta != current.Points[0].PathS - previous.Points[0].PathS,
    "handoff does not compare local PathS origins");
```

Projection failure must return unavailable `DeltaReferenceS` while preserving world/velocity/acceleration deltas.

- [ ] **Step 4: Run RED**

Expected: compilation failure because builders and metrics do not exist.

- [ ] **Step 5: Implement chart and handoff builders**

For jerk, iterate only while `index + 1 < trajectory.Points.Count` and use the interval start time/value. For LS, project every world pose to the complete active `DirectionSegmentView` and use `segment.SourceStartArcLength + projection.ReferenceS`. For ST, retain `point.TimeFromStart` and the plan-local `point.PathS`.

Use `TrajectorySampler.TrySample` at:

```csharp
double previousTime = (current.Metadata.EffectiveAtUtc - previous.Metadata.EffectiveAtUtc).TotalSeconds;
```

Compare the sampled previous point with current point zero. Project both to the same full segment for `DeltaReferenceS`; never subtract local `PathS`.

- [ ] **Step 6: Implement dynamic snapshot composition**

Dynamic world overlays contain real pose, current trajectory, previous trajectory, active-segment highlight and current projected horizon. Status values include active segment/direction, planning status, mode, terminal type, elapsed, trajectory age, projection failures, rolling constraint semantics, terminal `v/a`, handoff deltas, and unmet gear-confirmation condition. Cycle summary contains no full point arrays.

- [ ] **Step 7: Run GREEN**

```powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-core-all
```

Expected: both commands PASS; the existing rolling service regression still has 21 knots and nonzero rolling terminal speed.

- [ ] **Step 8: Commit the visualization adapter**

```powershell
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationKinematicChartBuilder.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationHandoffAnalyzer.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDynamicSnapshotBuilder.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs
git commit -m "feat: visualize EM rolling kinematics"
```

---

### Task 6: MovementTest web lifecycle and optional Painter fallback

**Files:**

- Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationVisualizationPublisher.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs`

**Interfaces:**

- `TrajectoryObservationVisualizationPublisher.Start(...)` creates the generic session and returns the URI.
- Internal `ITrajectoryObservationVisualizationSink` defines `Start`, `Publish`, and `Stop`; the production adapter wraps `PlanningVisualizationSession`, while tests inject a throwing sink without mocking the library.
- `TryPublish(...)` enforces the configured cadence and fuses on the first adapter/server fault.
- `Stop()` is idempotent.
- Native Painter objects are constructed only when `EnableNativePainterVisualization=true`.

- [ ] **Step 1: Write failing lifecycle/isolation checks**

Add a fake visualization sink whose `Publish` throws. Verify the wrapper logs/disables once, later calls are no-ops, and the `TrajectoryObservationLoop` continues producing ticks. Add a source audit asserting the MovementTest has no static eager `new TrajectoryObservationPresentation()` and conditionally creates it only under the native Painter flag.

Add a cadence check: observer ticks at 20 Hz with web refresh at 10 Hz produce no more than 11 snapshots over one second, including the initial snapshot.

- [ ] **Step 2: Run RED**

Expected: failures because the publisher does not exist and Painter creation is eager.

- [ ] **Step 3: Implement publisher cadence and fuse**

Use this internal seam and facade surface:

```csharp
internal interface ITrajectoryObservationVisualizationSink
{
    PlanningVisualizationSessionInfo Start(PlanningVisualizationOptions options,
        PlanningVisualizationStaticSnapshot snapshot);
    void Publish(PlanningVisualizationDynamicSnapshot snapshot);
    void Stop();
}

internal sealed class TrajectoryObservationVisualizationPublisher
{
    internal TrajectoryObservationVisualizationPublisher(TrajectoryObservationSettings settings,
        ITrajectoryObservationVisualizationSink sink, Action<string> log);
    internal PlanningVisualizationSessionInfo Start(PlanningVisualizationStaticSnapshot snapshot);
    internal bool TryPublish(DateTimeOffset now,
        Func<PlanningVisualizationDynamicSnapshot> snapshotFactory);
    internal void Stop();
    internal string FaultReason { get; }
}
```

The production sink constructs and disposes one `PlanningVisualizationSession`; the fake sink records calls or throws.

The publisher stores `nextPublishAtUtc`. When enabled and healthy:

```csharp
if (now < nextPublishAtUtc) return false;
nextPublishAtUtc = now + TimeSpan.FromSeconds(1d / settings.WebRefreshRateHz);
session.Publish(dynamicBuilder.Build(...));
return true;
```

Catch any exception from build/start/publish, call `session.Stop()`, set one immutable Chinese fault reason, invoke the supplied log callback once, and return false thereafter.

- [ ] **Step 4: Integrate successful bootstrap and browser launch**

After bootstrap succeeds and before the loop begins, build the static snapshot and start the web session only when enabled. Log the full tokenized URI. If auto-open is enabled, call:

```csharp
Process.Start(new ProcessStartInfo
{
    FileName = sessionInfo.Uri.AbsoluteUri,
    UseShellExecute = true,
});
```

Catch browser-launch exceptions separately and keep the web session running.

- [ ] **Step 5: Make Painter lazy and conditional**

Remove static eager `Presentation`. Create a session-local presentation only when enabled, pass null otherwise, and guard all `DrawWorld/DrawLs/DrawSt/ClearAll` calls. Stopping or replacing a session stops its web publisher and clears only an existing Painter. Bootstrap failure still logs through UI/console even when both visualization modes are off.

- [ ] **Step 6: Publish dynamic snapshots from real ticks**

Use `controller.ActiveSegment` for LS/ST building and world highlight. Pass the completed cycle, elapsed time, in-flight flag, segment state, current/previous trajectories, live state and current sample into `TryPublish`. Do not serialize or wait in the observer loop.

- [ ] **Step 7: Run GREEN and source audit**

```powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
rg -n "SendXYThSpeed|SendMotion|DriveStop|PredefinedDriveStop|AccumulateSpeed" ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest
```

Expected: check PASS and `rg` finds no actuator call in runtime observer sources.

- [ ] **Step 8: Commit MovementTest lifecycle**

```powershell
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationVisualizationPublisher.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationVisualizationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: host EM observation dashboard"
```

---

### Task 7: Plugin packaging and operator documentation

**Files:**

- Modify: `ClumsyPilot/scripts/Publish-ClumsyPilotPlugin.ps1`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/PluginPackagingChecks.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md`
- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md`

**Interfaces:**

- Plugin output adds `plugins/TrajectoryPlanningVisualization.dll` beside `ClumsyPilot.dll`.
- The publisher resolves the visualization DLL only as a sibling of the explicit `ManagedDll`; it does not search `PATH` or current directory.

- [ ] **Step 1: Write failing package-tree check**

Change the expected sorted tree to:

```text
plugins/ClumsyPilot.dll|
plugins/TrajectoryPlanningVisualization.dll|
plugins/licenses/OSQP-LICENSE.txt|
plugins/licenses/OSQP-NOTICE.txt|
plugins/licenses/OSQP-VERSION.txt|
plugins/osqp.dll
```

Also use `AssemblyName.GetAssemblyName` to verify the new file is a managed assembly named `TrajectoryPlanningVisualization`.

- [ ] **Step 2: Run RED**

```powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- plugin-package
```

Expected: failure because the publish script omits the visualization DLL.

- [ ] **Step 3: Update the atomic publisher**

Resolve:

```powershell
$visualizationDll = Join-Path ([System.IO.Path]::GetDirectoryName($managedDllPath)) 'TrajectoryPlanningVisualization.dll'
if (-not [System.IO.File]::Exists($visualizationDll)) {
    throw "Visualization DLL does not exist beside managed DLL: $visualizationDll"
}
```

Copy it into staging before the atomic directory swap. Keep all existing drive-root/workspace-root protections, OSQP hash validation and recovery behavior unchanged.

- [ ] **Step 4: Run package GREEN**

Run `plugin-package` twice. Expected: PASS both times and no stale staging/backup directories.

- [ ] **Step 5: Update both READMEs**

The MovementTest README must document:

- how to enable web/Painter modes;
- localhost/token URL behavior;
- Chinese scientific chart conventions;
- `ReferenceS` versus local `PathS`;
- jerk interval semantics;
- rolling/approach/exact-stop labels;
- active/completed/future segment colors;
- direction-confirmation conditions;
- browser closure and TestStop behavior.

The EM README plugin tree must include `TrajectoryPlanningVisualization.dll` and link to the observation README.

- [ ] **Step 6: Commit packaging and docs**

```powershell
git add -- ClumsyPilot/scripts/Publish-ClumsyPilotPlugin.ps1 ClumsyPilot/tests/EMPlannerVerificationHost/PluginPackagingChecks.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md
git commit -m "docs: package EM observation dashboard"
```

---

### Task 8: Full automated and manual acceptance

**Files:**

- No planned source changes. If a command fails, return to the owning task, add a focused failing regression there, implement the minimal correction, and rerun this acceptance task from Step 1.

**Interfaces:**

- Consumes: both verification hosts, the packaged plugin output, the deterministic smoke mode, and the deployed MovementTest entry.
- Produces: fresh automated evidence plus an explicit completed-or-pending vehicle checklist; it does not introduce a new runtime API.

- [ ] **Step 1: Run the complete automated suite**

```powershell
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
git diff --check
```

Expected: visualization host PASS; every `em-all` component PASS; main build exits `0`; no new whitespace errors. Record any pre-existing warnings separately rather than claiming they were introduced here.

- [ ] **Step 2: Run a local dashboard smoke session**

Run the deterministic smoke mode delivered by Plan 1:

```powershell
dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -- --smoke-seconds 30
```

Open the printed tokenized URI during the 30-second window and verify:

- Chinese titles/status and English/scientific axes;
- thin white-background scientific plots;
- all four tabs and all required charts;
- active segment/world horizon highlight;
- effective configuration values;
- stale-state display after sample publication stops;
- port release after the host exits.

This smoke command must publish synthetic snapshots only and must not reference MDCS or hardware.

- [ ] **Step 3: Perform the deployed vehicle observation checklist**

In a supervised safe environment:

1. Enable web and leave native Painter disabled.
2. Confirm `OBSERVE_ONLY` appears in UI/console/page.
3. Observe rolling, approach and exact-stop modes; confirm no forced rolling parking label.
4. Confirm jerk has `N-1` interval samples and no point-21 successor interval or `JerkLimitExceeded`.
5. Confirm current/previous handoff uses `Δposition/ΔReferenceS/Δv/Δa`.
6. At a real gear switch, confirm stop hold and three signed-speed samples precede `N -> N+1` highlight.
7. Close the browser and confirm planning cadence continues.
8. Stop MovementTest and confirm server/port/Painter cleanup and no actuator output.

- [ ] **Step 4: Commit only acceptance-driven fixes**

If Step 1–3 required code changes, commit each regression and fix with exact file paths. If no changes were required, do not create an empty commit.

Plan 2 is complete only after automated evidence is fresh and the vehicle-only checklist is explicitly reported as completed or, if the vehicle is unavailable, explicitly reported as pending rather than silently treated as passed.
