# EM observation MovementTest diagnostics

## Goal

Make a failed or pending EM planning cycle understandable during an on-vehicle observation test.  The test remains strictly observe-only and does not gain any chassis actuator call.

## Scope

`TrajectoryObservationMovementTest` will publish one shared diagnostic snapshot for every observer tick.  It will contain the current planning state, whether a trajectory was published, the planner failure reason or pending marker, planning version, and elapsed planning time.  The text deliberately excludes routine pose, selected-point, and command details when no trajectory exists: failure diagnostics must stay short and easy to scan.

The snapshot is delivered through two channels:

1. A terminal line prefixed with `[TrajectoryObserver]`, emitted at session start/stop/fault, once when each planning cycle starts, and once when each planning cycle completes.  This guarantees every individual failure is recorded, while the 50 ms observer tick cannot flood the console.
2. Persistent text drawn in the World painter.  The L-S and S-T empty states show a compact copy of the same planning state and reason, rather than only `No published trajectory available`.

The existing `Hedingben.ToastText` status remains for UI users.  It receives the same diagnostic text but is not the sole source of failure information.

## Status and failure rules

Every completed cycle prints the raw `EmPlanningStatus`, `published` flag, version, elapsed milliseconds, and the exact `FailureReason` returned by the planner.  No failure reason is replaced with a generic message.  Therefore the following independent failure classes remain distinguishable without adding a parallel exception hierarchy:

- `InvalidInput`, `UnsupportedMotionMode`, `StaleVehicleState`;
- `StateDirectionMismatch`, `InvalidReferencePath`, `ProjectionFailed`;
- `CorridorInfeasible`, `LateralInfeasible`, `LongitudinalInfeasible`, `StoppingDistanceInsufficient`;
- `SolverUnavailable`, `SolverTimedOut`;
- `ValidationFailed`, `Cancelled`, `Superseded`, `Failed`.

For example, a compact failed-cycle line is:

```text
[TrajectoryObserver] cycle=4 status=CorridorInfeasible published=False elapsed=18ms reason=... 
```

Before completion, the line reports `status=pending`; before the first cycle it reports `status=not-started`.  If an older trajectory is still safely published while a new cycle is pending, the text reports both the pending state and that trajectory ID.

## Data flow

`TrajectoryObservationLoopTick` already exposes `LatestCycle`, `LatestPlanningElapsed`, and `PlanningInFlight`.  A small pure formatter converts those fields plus the published trajectory state into operator text:

- before a completed cycle: `planning=pending` or `planning=not-started`;
- completed success: status, `published=True`, version and elapsed time;
- completed failure: status, `published=False`, version, elapsed time and the unmodified `FailureReason`;
- a published older trajectory during an in-flight replan: retain its trajectory identity while stating that replanning is pending.

Presentation consumes this formatted text independently of charts.  Thus it is visible even when `PublishedTrajectory` is null and no L-S/S-T samples can be built.

## Safety and error handling

Only text output and painter calls are added.  No `SendXYThSpeed`, motion command, brake, or wheel call is introduced.  If a planning Task faults, the existing session fault path remains responsible for cleanup and its exception is printed through the same terminal/UI status mechanism.

## Verification

The verification host gets checks for:

- a no-trajectory diagnostic that preserves a real failure reason and raw `EmPlanningStatus`;
- presentation empty-state text containing the diagnostic;
- one terminal record per cycle start and completion, with no duplicate 50 ms tick records;
- the existing source deny-list continuing to reject actuator calls.
