Files
ParkingRobot/docs/superpowers/plans/2026-08-05-em-observation-web-integration.md
T

695 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 13 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.