# EM Observation MovementTest 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:** Build a real-localization, observe-only MovementTest that creates a configurable start/goal map, plans Hybrid A* → Local G2 → EM trajectories, and shows world, LS, and ST diagnostics without sending a chassis command.

**Architecture:** Put map construction, planning bootstrap, rolling EM requests, trajectory observation, and LS/ST derivation in pure, testable classes. Keep MDCS reads, prompts, painters, background timing, and cancellation in one thin MovementTest host. The host may only read DetourInterface and BasicPilotBase.Chassis; its only control output is a displayed TrajectoryControlCommand.

**Tech Stack:** C# 10, netstandard2.0, existing Clumsy MovementTest/Painter UI, MDCS localization and chassis read APIs, Hybrid A*, Local G2, EM planner, OSQP, and EMPlannerVerificationHost.

## Global Constraints

- All map geometry and UI world coordinates are mm; Pose2D, velocities, and EM geometry are m, m/s, and rad.
- Bounds are exactly the start/goal axis-aligned rectangle expanded by MapPaddingMeters on all sides. Obstacles must fit these bounds; they must not enlarge them.
- Default settings are: padding 2.0 m, resolution 50 mm, replan 0.20 s, observer period 0.05 s.
- Capture world pose through DetourInterface.getCartLocation() and signed body-longitudinal velocity from BasicPilotBase.Chassis.GetCarSpeed(true).Vx. Create a monotonically increasing state sequence id.
- Output is TrajectoryControlCommand for display only. Do not invoke SendXYThSpeed, SendMotion, SendTh, AccumulateSpeed, ComputeWheelsGeometrically, brake/wheel adapter methods, or a geometric controller.
- Keep runtime source under ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest. Do not change the existing coarse-path factory, whose unrelated manual demo uses an 8 m expansion.
- Stop/cancel must cancel worker activity and clear the World, LS, and ST painter layers.

---

## File structure

| File | Responsibility |
| --- | --- |
| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs | Settings, manual-obstacle DTOs, validation, exact map-job construction. |
| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs | Hybrid A* + Local G2 bootstrap, rolling EM requests, time observation, LS/ST models. |
| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs | Three painter layers and presentation text; no MDCS/hardware use. |
| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs | Discoverable test, MDCS state reader, prompts, background session, console, cancellation. |
| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md | Operator configuration, layer interpretation, unit and safety guidance. |
| ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs | Deterministic regression checks and an actuator-call source audit. |
| ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs | Adds the trajectory-observation command. |

### Task 1: Configuration and exact rectangle-map inputs

**Files:**

- Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs
- Create: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs

**Interfaces:**

- Consumes: Pose2D, VehicleParameters, PlanningMapRequest, MapBoundsMm, ManualObstacleSource, CircleObstacle, AxisAlignedRectangleObstacle.
- Produces: TrajectoryObservationSettings.Validate(), TrajectoryObservationObstacle.Circle(double, double, double), TrajectoryObservationObstacle.Rectangle(double, double, double, double), and TrajectoryObservationSetupFactory.CreateBootstrapJob(Pose2D, Pose2D, TrajectoryObservationSettings, IReadOnlyList<TrajectoryObservationObstacle>, long).

- [ ] **Step 1: Write failing map-bounds and obstacle checks**

Create the verification host class and invoke it with a new trajectory-observation argument:

~~~csharp
internal static class TrajectoryObservationChecks
{
    public static void Run()
    {
        VerifiesStartGoalBoundsUseOnlyConfiguredPadding();
        RejectsObstacleOutsideConfiguredBounds();
    }

    private static void VerifiesStartGoalBoundsUseOnlyConfiguredPadding()
    {
        var settings = new TrajectoryObservationSettings
        {
            MapPaddingMeters = 2d,
            MapResolutionMillimeters = 50f,
        };
        CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob(
            new Pose2D(10d, -5d, 0d), new Pose2D(13d, -1d, 0d), settings,
            Array.Empty<TrajectoryObservationObstacle>(), 17L);

        Verification.NearlyEqual(8000d, job.MapRequest.Bounds.XMin, "observer map x min");
        Verification.NearlyEqual(15000d, job.MapRequest.Bounds.XMax, "observer map x max");
        Verification.NearlyEqual(-7000d, job.MapRequest.Bounds.YMin, "observer map y min");
        Verification.NearlyEqual(1000d, job.MapRequest.Bounds.YMax, "observer map y max");
        Verification.NearlyEqual(50d, job.MapRequest.ResolutionMm, "observer map resolution");
    }
}
~~~

Modify Program.Main to accept trajectory-observation, call TrajectoryObservationChecks.Run(), then write PASS trajectory-observation. Add the same call to em-all.

- [ ] **Step 2: Run the new check to prove it fails**

Run:

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

Expected: compilation fails because TrajectoryObservationSettings and TrajectoryObservationSetupFactory do not exist.

- [ ] **Step 3: Implement the contracts and factory**

Create the editable configuration contract:

~~~csharp
public sealed class TrajectoryObservationSettings
{
    public double MapPaddingMeters { get; set; } = 2d;
    public float MapResolutionMillimeters { get; set; } = 50f;
    public double ReplanPeriodSeconds { get; set; } = 0.20d;
    public double ObserverPeriodSeconds { get; set; } = 0.05d;
    public double VehicleLengthMeters { get; set; } = 0.80d;
    public double VehicleWidthMeters { get; set; } = 0.60d;
    public double SafetyMarginMeters { get; set; } = 0.05d;
    public double MaximumCurvaturePerMeter { get; set; } = 1d / 1.20d;

    public void Validate();
    public VehicleParameters CreateVehicle();
}
~~~

Implement finite/positive validation. Implement the obstacle as world-mm circle or axis-aligned rectangle with GetBounds() and ToMapObstacle(). Build bounds with the following exact calculation, rounded outward to the configured grid:

~~~csharp
double padMm = settings.MapPaddingMeters * 1000d;
var bounds = new MapBoundsMm(
    ToGridLower(Math.Min(start.X, goal.X) * 1000d - padMm, settings.MapResolutionMillimeters),
    ToGridUpper(Math.Max(start.X, goal.X) * 1000d + padMm, settings.MapResolutionMillimeters),
    ToGridLower(Math.Min(start.Y, goal.Y) * 1000d - padMm, settings.MapResolutionMillimeters),
    ToGridUpper(Math.Max(start.Y, goal.Y) * 1000d + padMm, settings.MapResolutionMillimeters));
~~~

Reject an obstacle unless its full envelope is contained in bounds. With zero obstacles set AllowExplicitEmptyMap true. Otherwise construct exactly one required ManualObstacleSource named trajectory-observer-manual with the supplied positive snapshot version. Return a CoarsePathPlanningJob with new HybridAStarConfiguration, StartDirection = null, and GoalDirection = GoalDirectionConstraint.Any.

- [ ] **Step 4: Run focused and existing checks**

Run:

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

Expected: both exit 0 and print PASS trajectory-observation and PASS foundation.

- [ ] **Step 5: Commit the input layer**

~~~powershell
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs
git commit -m "feat: add observation test map inputs"
~~~

### Task 2: Pure planning bootstrap, time observation, and LS/ST derivation

**Files:**

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

**Interfaces:**

- Consumes: CoarsePathPlanningService, PathSmoothingService, EmPlanningCoordinator, TrajectoryExecutor, FrenetProjector, and caller-supplied VehicleMotionState.
- Produces: TrajectoryObservationBootstrapper.Bootstrap(CoarsePathPlanningJob, CancellationToken), TrajectoryObservationController.StartCycle(DateTimeOffset, VehicleMotionState, CancellationToken), TrajectoryObservationController.Observe(DateTimeOffset, VehicleMotionState), and TrajectoryObservationCharts.Build(EmTrajectory, DirectionSegmentView, double).

- [ ] **Step 1: Add failing chart and time-sampling checks**

Extend TrajectoryObservationChecks.Run() by adding VerifiesLsAndStUsePublishedTrajectoryData(). Use a fixed two-point EmTrajectory whose EffectiveAtUtc is 2026-08-04T00:00:00Z, with TimeFromStart values 0 and 1, PathS values 4 and 5, and known signed speeds. Assert that Build returns two ST samples (0,4) and (1,5), two speed samples, and the expected LS projection count. Call Observe at 00:00:00.500Z and assert that TrajectoryExecutor selected an interpolated point with TimeFromStart == 0.5d.

- [ ] **Step 2: Run the new check to prove it fails**

Run:

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

Expected: compilation fails because TrajectoryObservationCharts and TrajectoryObservationController do not exist.

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

Bootstrap must use exactly this success gate:

~~~csharp
CoarsePathPlanningJobResult coarse = coarseService.Plan(job, cancellationToken);
if (coarse.PlanningResult.Status != PlanningStatus.Success)
    return TrajectoryObservationBootstrapResult.FromFailure(
        job, coarse, null, "Coarse planning status: " + coarse.PlanningResult.Status);

var smoothingRequest = new PathSmoothingRequest(
    CopyFiniteClearance(coarse.PlanningResult.Path, coarse.MapResult.Map),
    coarse.PlanningResult.Segments, coarse.MapResult.Map, job.Vehicle,
    new PathSmoothingConfiguration());
PathSmoothingResult smooth = smoothingService.Smooth(smoothingRequest, cancellationToken);
if (!IsPublishedSmoothingStatus(smooth.Status))
    return TrajectoryObservationBootstrapResult.FromFailure(
        job, coarse, smooth, smooth.Diagnostics.TerminationReason);
return TrajectoryObservationBootstrapResult.Success(job, coarse, smooth, ReferencePathSegmenter.Create(smooth));
~~~

IsPublishedSmoothingStatus accepts only Complete, PartialImprovement, NotNeeded, and Unchanged. CopyFiniteClearance replaces a positive-infinite clearance with the finite map diagonal before copying each CoarsePathPoint.

TrajectoryObservationController owns EmPlanningCoordinator and TrajectoryExecutor. For a replan it creates:

~~~csharp
var request = new EmPlanningRequest(
    bootstrap.SmoothedPath, bootstrap.Map, bootstrap.Job.Vehicle, state, configuration,
    segmentIndex, coordinator.PublishedTrajectory, now, now,
    sessionId + "-trajectory-" + cycleId, sessionId + "-reference",
    coordinator.PublishedTrajectory?.Metadata.TrajectoryId ?? string.Empty,
    EmMotionModel.NonholonomicForwardReverse);
return coordinator.PlanLatestAsync(new PlanningCycleInput(request, now), cancellationToken);
~~~

Set configuration.Scheduling.ReplanPeriodSeconds from settings. Initial observation mode always uses segmentIndex 0. Observe must use PublishedTrajectory only; when non-null call UpdateCommand(now, state, trajectory, trajectory.Metadata.Direction, trajectory.Metadata.Direction, true) and return the selected point, command, and executor state for display only.

Build LS/ST from published data alone:

~~~csharp
ls.Add(new TrajectoryObservationLsSample(
    segment.SourceStartArcLength + projection.ReferenceS, projection.LateralOffset));
st.Add(new TrajectoryObservationStSample(point.TimeFromStart, point.PathS));
speed.Add(new TrajectoryObservationSpeedSample(point.TimeFromStart, point.SignedLongitudinalVelocity));
~~~

Use seeded FrenetProjector calls and count failed projections. No pipeline class may reference UI, DetourInterface, BasicPilotBase, or a hardware class.

- [ ] **Step 4: Run diagnostics and regression checks**

Run:

~~~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: every command exits 0.

- [ ] **Step 5: Commit the pure pipeline**

~~~powershell
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: add EM observation planning pipeline"
~~~

### Task 3: Presentation layers and observation text

**Files:**

- Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs
- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs

**Interfaces:**

- Consumes: bootstrap result, observation result, and chart data.
- Produces: TrajectoryObservationPresentation.DrawWorld(TrajectoryObservationBootstrapResult, TrajectoryObservationObservation), DrawLs(TrajectoryObservationCharts), DrawSt(TrajectoryObservationCharts), ClearAll(), and TrajectoryObservationPresentationText.Create(TrajectoryObservationObservation, TrajectoryObservationCharts).

- [ ] **Step 1: Add a failing presentation-text check**

Assert that TrajectoryObservationPresentationText.Create(observation, charts) contains the literal OBSERVE_ONLY: no chassis command is sent., selected point time/path-S, signed speed, yaw rate, and LS projection failure count. The check must not instantiate a Painter.

- [ ] **Step 2: Run the check to verify it fails**

Run:

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

Expected: compilation fails because TrajectoryObservationPresentationText does not exist.

- [ ] **Step 3: Implement the three painters**

Create exactly these named layers:

~~~csharp
worldPainter = UI.GetPainter("TrajectoryObserver.World", true);
lsPainter = UI.GetPainter("TrajectoryObserver.LS", true);
stPainter = UI.GetPainter("TrajectoryObserver.ST", true);
~~~

DrawWorld clears only worldPainter then draws map bounds/grid/occupied cells, start, goal, coarse path, Local G2 path, real pose, and latest EM path. Convert every planner position from m to mm before calling DrawLine, DrawCircle, or DrawText.

DrawLs draws axes plus s-l samples. DrawSt draws t-s and a vertically separated t-v series with a legend. A missing trajectory draws a status string instead of throwing. ClearAll invokes Clear on all three painters and performs no other action.

- [ ] **Step 4: Run visual-model and compile verification**

Run:

~~~powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore
~~~

Expected: trajectory-observation passes and the project has zero compile errors.

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

~~~powershell
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: visualize EM observation diagnostics"
~~~

### Task 4: MDCS read-only MovementTest host

**Files:**

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

**Interfaces:**

- Consumes: DetourInterface.getCartLocation(), BasicPilotBase.Chassis.GetCarSpeed(true), setup/controller/presentation APIs.
- Produces: a [MovementTest(name = "EM轨迹规划观察闭环测试")] entry with Test() and TestStop().

- [ ] **Step 1: Write a failing actuator-free source audit**

Add VerifiesObservationSourceHasNoActuatorCalls() to TrajectoryObservationChecks.Run() and implement it in the verification host. It reads the observation runtime source files and fails on any of these tokens:

~~~csharp
new[]
{
    ".SendXYThSpeed(", ".SendMotion(", ".SendTh(", ".AccumulateSpeed(",
    ".ComputeWheelsGeometrically(", ".DriveStop(", ".PredefinedDriveStop("
}
~~~

The audit strings live only in the test host; none may appear in the new runtime observation files.

- [ ] **Step 2: Run the audit before the host exists**

Run:

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

Expected: the check fails because MovementTest.TrajectoryObservationTest.cs is missing.

- [ ] **Step 3: Implement the host and lifecycle**

Use this discoverable configuration:

~~~csharp
[MovementTest(name = "EM轨迹规划观察闭环测试")]
public sealed class TrajectoryObservationMovementTest : MovementTest
{
    public double GoalXmm = double.NaN;
    public double GoalYmm = double.NaN;
    public double GoalYawDeg = 0d;
    public double MapPaddingMeters = 2d;
    public float MapResolutionMm = 50f;
    public double ReplanPeriodSeconds = 0.20d;
    public double ObserverPeriodSeconds = 0.05d;

    public override void Test();
    public override void TestStop();
}
~~~

When GoalXmm or GoalYmm is non-finite, prompt for all goal values with the same finite parser/UI.GetInput pattern as CoarsePathPlanningTest. Prompt for 0–20 manual obstacles (circle or rectangle) and freeze all inputs before Task.Run begins.

The MDCS reader must use only this read path:

~~~csharp
var location = DetourInterface.getCartLocation();
if (location == null) throw new InvalidOperationException("Live localization is unavailable.");
if (BasicPilotBase.Chassis == null) throw new InvalidOperationException("Live chassis read interface is unavailable.");
var speed = BasicPilotBase.Chassis.GetCarSpeed(true);
return new VehicleMotionState(
    new Pose2D(location.x / 1000d, location.y / 1000d, location.th * Math.PI / 180d),
    speed.Vx, null, DateTimeOffset.UtcNow, Interlocked.Increment(ref stateSequence));
~~~

Bootstrap once in a cancellable Task.Run. After success, run Task.Delay(TimeSpan.FromSeconds(ObserverPeriodSeconds), token) between ticks. At each tick capture exactly one state, start a cycle only when controller.ShouldStartCycle(now), observe the latest published trajectory, draw all layers, and print a throttled status. Construct the service as new EmPlanningService(new OsqpNativeSolver()).

Every status includes:

~~~text
OBSERVE_ONLY: no chassis command is sent.
~~~

When a GearSwitch trajectory reaches its final time, draw and print 等待真实档位/方向确认；观察模式不会推进下一方向段, leave segment index 0, and do not create a direction-change action. Goal and rolling-stop commands may only be logged.

Use a lock/session id pattern matching CoarsePathPlanningTestRunner: replace the active CancellationTokenSource, cancel the old source without waiting, and allow only the current session to draw or log. TestStop cancels, disposes after task completion, and calls presentation.ClearAll.

Write README.md with configuration fields/units, obstacle examples, default values, chart interpretations, the VelocityX/VelocityY world-frame warning, and the explicit no-driving limitation.

- [ ] **Step 4: Verify runner safety and integration build**

Run:

~~~powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore
rg -n 'SendXYThSpeed\(|SendMotion\(|SendTh\(|AccumulateSpeed\(|ComputeWheelsGeometrically\(|DriveStop\(|PredefinedDriveStop\(' ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest -g '*.cs'
~~~

Expected: host and build exit 0. The rg command exits 1 because no runtime observation file calls a forbidden actuator method.

- [ ] **Step 5: Commit the MovementTest**

~~~powershell
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: add read-only EM observation movement test"
~~~

### Task 5: End-to-end regression and operator handoff

**Files:**

- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
- Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md

**Interfaces:**

- Consumes: completed observation-test components and existing EMPlannerVerificationHost checks.
- Produces: a reproducible all-up verification command and a launch/stop checklist.

- [ ] **Step 1: Add a failing bootstrap regression**

Use an empty-map setup with start (0.5, 0.5, 0) m and goal (3.5, 0.5, 0) m. Assert that bootstrap returns a successful map, PlanningStatus.Success, a publishable smoothing result, and at least one DirectionSegmentView. This check does not run native OSQP.

- [ ] **Step 2: Run the check to confirm its failure**

Run:

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

Expected: the assertion identifies a missing or incorrect bootstrap result.

- [ ] **Step 3: Make the smallest corrective change**

Correct only TrajectoryObservationSetupFactory or TrajectoryObservationBootstrapper so the empty-map request produces a planning-ready map and publishable Local G2 path. Preserve the exact bounds rule and do not add UI, MDCS, or hardware dependencies to pure classes.

- [ ] **Step 4: Run all required evidence checks**

Run:

~~~powershell
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 --no-restore
git diff --check
~~~

Expected: every command exits 0. In the vehicle UI, the entry appears as EM轨迹规划观察闭环测试 and starting/running/stopping it does not issue any chassis, motor, steering, or brake output.

- [ ] **Step 5: Commit final verification/documentation**

~~~powershell
git add -- ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md
git commit -m "test: verify EM observation movement test"
~~~

## Plan self-review

**Spec coverage:** Task 1 provides configurable start/goal map bounds, vehicle settings, and manual obstacles. Task 2 covers Hybrid A*, Local G2, rolling EM, time sampling, and derived LS/ST. Task 3 creates World/LS/ST painters. Task 4 reads live MDCS state, prints observation diagnostics, handles gear-switch observation, and ensures cancellation/no-write behavior. Task 5 supplies an end-to-end fixture and final evidence.

**Placeholder scan:** Every task names concrete files, commands, interface names, inputs, expected behavior, and commit content; no deferred implementation markers remain.

**Type consistency:** Map code produces PlanningMapRequest and CoarsePathPlanningJob; bootstrap produces PathSmoothingResult and DirectionSegmentView; rolling planning consumes VehicleMotionState and EmPlanningRequest; UI consumes EmTrajectory, TrajectoryControlCommand, and chart samples without changing EM contracts.
